From 36fd48c1e883db71ce592b155ee9cd32d1383d13 Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Wed, 1 Oct 2025 00:49:41 +0530 Subject: [PATCH 01/14] Enhance response formatting with new types and utility functions Added TypeScript types for text and image content, along with a unified ToolResponse type. Introduced utility functions for creating text and image content, and for formatting tool responses. This improves type safety and clarity in handling different response formats. --- src/utils/formatResponse.ts | 41 ++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/utils/formatResponse.ts b/src/utils/formatResponse.ts index 64bd97c..3903734 100644 --- a/src/utils/formatResponse.ts +++ b/src/utils/formatResponse.ts @@ -1,9 +1,48 @@ -export function formatResponse(response: any) { +export type TextContent = { + type: "text"; + text: string; +} + +export type ImageContent = { + type: "image"; + data: string; + mimeType: string; +} + +export type Content = TextContent | ImageContent; + +export type ToolResponse = { + content: Content[]; +} + +export function formatResponse(response: any): ToolResponse { return { content: [{ type: "text" as "text", text: JSON.stringify(response) }], }; } +export function textContent(response: any): TextContent { + return { + type: "text", + text: JSON.stringify(response) + }; +} + +export function imageContent(data: string, mimeType: string): ImageContent { + return { + type: "image", + data, + mimeType, + }; +} + +export function toolResponse(contentItems: Content[]): ToolResponse { + return { + content: contentItems, + }; +} + + // https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2 export function formatErrorResponse(error: any) { return { From e30e6cf14a1c95410c8e5cf0daffc5eee326a80e Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Wed, 1 Oct 2025 00:50:43 +0530 Subject: [PATCH 02/14] CMS Tool consolidation --- src/tools/cms.ts | 636 ++++++++++++++++++++++------------------------- 1 file changed, 299 insertions(+), 337 deletions(-) diff --git a/src/tools/cms.ts b/src/tools/cms.ts index c511b79..205efb3 100644 --- a/src/tools/cms.ts +++ b/src/tools/cms.ts @@ -5,241 +5,220 @@ import { requestOptions } from "../mcp"; import { OptionFieldSchema, ReferenceFieldSchema, + SiteIdSchema, StaticFieldSchema, WebflowCollectionsCreateRequestSchema, WebflowCollectionsFieldUpdateSchema, - WebflowCollectionsItemsCreateItemLiveRequestSchema, - WebflowCollectionsItemsCreateItemRequestSchema, + // WebflowCollectionsItemsCreateItemLiveRequestSchema, WebflowCollectionsItemsListItemsRequestSortBySchema, WebflowCollectionsItemsListItemsRequestSortOrderSchema, - WebflowCollectionsItemsUpdateItemsLiveRequestSchema, + // WebflowCollectionsItemsUpdateItemsLiveRequestSchema, WebflowCollectionsItemsUpdateItemsRequestSchema, } from "../schemas"; -import { formatErrorResponse, formatResponse } from "../utils"; +import { Content, formatErrorResponse, formatResponse, textContent, toolResponse } from "../utils"; +import { CollectionsCreateRequest, FieldCreate } from "webflow-api/api"; +import { FieldUpdate } from "webflow-api/api/resources/collections/resources/fields"; +import { + //ItemsCreateItemLiveRequest, + //ItemsUpdateItemsLiveRequest, + ItemsDeleteItemsRequest, ItemsListItemsRequest, + ItemsUpdateItemsRequest } from "webflow-api/api/resources/collections/resources/items"; export function registerCmsTools( server: McpServer, getClient: () => WebflowClient ) { - // GET https://api.webflow.com/v2/sites/:site_id/collections - server.tool( - "collections_list", - "List all CMS collections in a site. Returns collection metadata including IDs, names, and schemas.", - { - site_id: z.string().describe("Unique identifier for the Site."), - }, - async ({ site_id }) => { - try { - const response = await getClient().collections.list( - site_id, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const getCollectionList = async (arg:{siteId:string})=>{ + const response = await getClient().collections.list( + arg.siteId, + requestOptions + ); + return response; + } + const getCollectionDetails = async (arg:{collection_id:string})=>{ + const response = await getClient().collections.get( + arg.collection_id, + requestOptions + ); + return response; + } - // GET https://api.webflow.com/v2/collections/:collection_id - server.tool( - "collections_get", - "Get detailed information about a specific CMS collection including its schema and field definitions.", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - }, - async ({ collection_id }) => { - try { - const response = await getClient().collections.get( - collection_id, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const createCollection = async (arg:{siteId:string, request: CollectionsCreateRequest})=>{ + const response = await getClient().collections.create( + arg.siteId, + arg.request, + requestOptions + ); + return response; + } - // POST https://api.webflow.com/v2/sites/:site_id/collections - server.tool( - "collections_create", - "Create a new CMS collection in a site with specified name and schema.", - { - site_id: z.string().describe("Unique identifier for the Site."), - request: WebflowCollectionsCreateRequestSchema, - }, - async ({ site_id, request }) => { - try { - const response = await getClient().collections.create( - site_id, - request, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const createCollectionStaticField = async (arg:{collection_id:string, request: FieldCreate})=>{ + const response = await getClient().collections.fields.create( + arg.collection_id, + arg.request, + requestOptions + ); + return response; + } - // POST https://api.webflow.com/v2/collections/:collection_id/fields - server.tool( - "collection_fields_create_static", - "Create a new static field in a CMS collection (e.g., text, number, date, etc.).", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: StaticFieldSchema, - }, - async ({ collection_id, request }) => { - try { - const response = await getClient().collections.fields.create( - collection_id, - request, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const createCollectionOptionField = async (arg:{collection_id:string, request: FieldCreate})=>{ + const response = await getClient().collections.fields.create( + arg.collection_id, + arg.request, + requestOptions + ); + return response; + } + const createCollectionReferenceField = async (arg:{collection_id:string, request: FieldCreate})=>{ + const response = await getClient().collections.fields.create( + arg.collection_id, + arg.request, + requestOptions + ); + return response; + } - // POST https://api.webflow.com/v2/collections/:collection_id/fields - server.tool( - "collection_fields_create_option", - "Create a new option field in a CMS collection with predefined choices.", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: OptionFieldSchema, - }, - async ({ collection_id, request }) => { - try { - const response = await getClient().collections.fields.create( - collection_id, - request, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const updateCollectionField = async (arg:{collection_id:string, field_id:string, request: FieldUpdate})=>{ + const response = await getClient().collections.fields.update( + arg.collection_id, + arg.field_id, + arg.request, + requestOptions + ); + return response; + } - // POST https://api.webflow.com/v2/collections/:collection_id/fields - server.tool( - "collection_fields_create_reference", - "Create a new reference field in a CMS collection that links to items in another collection.", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: ReferenceFieldSchema, - }, - async ({ collection_id, request }) => { - try { - const response = await getClient().collections.fields.create( - collection_id, - request, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + // const createCollectionItemsLive = async (arg:{collection_id:string, request: ItemsCreateItemLiveRequest})=>{ + // const response = await getClient().collections.items.createItemLive( + // arg.collection_id, + // arg.request, + // requestOptions + // ); + // return response; + // } + // const updateCollectionItemsLive = async (arg:{collection_id:string, request: ItemsUpdateItemsLiveRequest})=>{ + // const response = await getClient().collections.items.updateItemsLive( + // arg.collection_id, + // arg.request, + // requestOptions + // ); + // return response; + // } - // PATCH https://api.webflow.com/v2/collections/:collection_id/fields/:field_id - server.tool( - "collection_fields_update", - "Update properties of an existing field in a CMS collection.", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - field_id: z.string().describe("Unique identifier for the Field."), - request: WebflowCollectionsFieldUpdateSchema, - }, - async ({ collection_id, field_id, request }) => { - try { - const response = await getClient().collections.fields.update( - collection_id, - field_id, - request, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const listCollectionItems = async (arg:{collection_id:string, request: ItemsListItemsRequest})=>{ + const response = await getClient().collections.items.listItems( + arg.collection_id, + arg.request, + requestOptions + ); + return response; + } - // POST https://api.webflow.com/v2/collections/:collection_id/items/live - // NOTE: Cursor agent seems to struggle when provided with z.union(...), so we simplify the type here - server.tool( - "collections_items_create_item_live", - "Create and publish new items in a CMS collection directly to the live site.", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: WebflowCollectionsItemsCreateItemLiveRequestSchema, - }, - async ({ collection_id, request }) => { - try { - const response = await getClient().collections.items.createItemLive( - collection_id, - request, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const createCollectionItems = async (arg:{collection_id:string, request: { + cmsLocaleIds?: string[]; + isArchived?: boolean; + isDraft?: boolean; + fieldData: { + name: string; + slug: string; + [key: string]: any; + }[]; + }})=>{ - // PATCH https://api.webflow.com/v2/collections/:collection_id/items/live - server.tool( - "collections_items_update_items_live", - "Update and publish existing items in a CMS collection directly to the live site.", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: WebflowCollectionsItemsUpdateItemsLiveRequestSchema, - }, - async ({ collection_id, request }) => { - try { - const response = await getClient().collections.items.updateItemsLive( - collection_id, - request, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const response = await getClient().collections.items.createItems( + arg.collection_id, + { + cmsLocaleIds: arg.request.cmsLocaleIds, + isArchived: arg.request.isArchived, + isDraft: arg.request.isDraft, + fieldData: arg.request.fieldData, + }, + requestOptions + ); + return response; + } - // GET https://api.webflow.com/v2/collections/:collection_id/items - server.tool( - "collections_items_list_items", - "List items in a CMS collection with optional filtering and sorting.", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), + const updateCollectionItems = async (arg:{collection_id:string, request: ItemsUpdateItemsRequest})=>{ + const response = await getClient().collections.items.updateItems( + arg.collection_id, + arg.request, + requestOptions + ); + return response; + } + const publishCollectionItems = async (arg:{collection_id:string, request: { + itemIds: string[]; + }})=>{ + const response = await getClient().collections.items.publishItem( + arg.collection_id, + { + itemIds: arg.request.itemIds, + }, + requestOptions + ); + return response; + } + const deleteCollectionItems = async (arg:{collection_id:string, request: ItemsDeleteItemsRequest})=>{ + const response = await getClient().collections.items.deleteItems( + arg.collection_id, + arg.request, + requestOptions + ); + return response; + } + + server.tool("data_cms_tool","Data tool - CMS tool to perform actions like get collection list, get collection details, create collection, create collection static field, create collection option field, create collection reference field, update collection field, create collection item live, update collection items live, list collection items, create collection item, update collection items, publish collection items, delete collection item",{ + actions:z.array( + z.object({ + // GET https://api.webflow.com/v2/sites/:site_id/collections + get_collection_list:z.object({ + ...SiteIdSchema, + }).optional().describe("List all CMS collections in a site. Returns collection metadata including IDs, names, and schemas."), + // GET https://api.webflow.com/v2/collections/:collection_id + get_collection_details:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + }).optional().describe("Get detailed information about a specific CMS collection including its schema and field definitions."), + // POST https://api.webflow.com/v2/sites/:site_id/collections + create_collection:z.object({ + ...SiteIdSchema, + request: WebflowCollectionsCreateRequestSchema, + }).optional().describe("Create a new CMS collection in a site with specified name and schema."), + // POST https://api.webflow.com/v2/collections/:collection_id/fields + create_collection_static_field:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + request: StaticFieldSchema, + }).optional().describe("Create a new static field in a CMS collection (e.g., text, number, date, etc.)."), + // POST https://api.webflow.com/v2/collections/:collection_id/fields + create_collection_option_field:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + request: OptionFieldSchema, + }).optional().describe("Create a new option field in a CMS collection with predefined choices."), + // POST https://api.webflow.com/v2/collections/:collection_id/fields + create_collection_reference_field:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + request: ReferenceFieldSchema, + }).optional().describe("Create a new reference field in a CMS collection that links to items in another collection."), + // PATCH https://api.webflow.com/v2/collections/:collection_id/fields/:field_id + update_collection_field:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + field_id: z.string().describe("Unique identifier for the Field."), + request: WebflowCollectionsFieldUpdateSchema, + }).optional().describe("Update properties of an existing field in a CMS collection."), + // // POST https://api.webflow.com/v2/collections/:collection_id/items/live + // //NOTE: Cursor agent seems to struggle when provided with z.union(...), so we simplify the type here + // create_collection_items_live:z.object({ + // collection_id: z.string().describe("Unique identifier for the Collection."), + // request: WebflowCollectionsItemsCreateItemLiveRequestSchema, + // }).optional().describe("Create and publish new items in a CMS collection directly to the live site."), + // // PATCH https://api.webflow.com/v2/collections/:collection_id/items/live + // update_collection_items_live:z.object({ + // collection_id: z.string().describe("Unique identifier for the Collection."), + // request: WebflowCollectionsItemsUpdateItemsLiveRequestSchema, + // }).optional().describe("Update and publish existing items in a CMS collection directly to the live site."), + // GET https://api.webflow.com/v2/collections/:collection_id/items + list_collection_items:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + request:z.object({ cmsLocaleId: z .string() .optional() @@ -263,134 +242,117 @@ export function registerCmsTools( ), sortBy: WebflowCollectionsItemsListItemsRequestSortBySchema, sortOrder: WebflowCollectionsItemsListItemsRequestSortOrderSchema, - }, - async ({ - collection_id, - cmsLocaleId, - offset, - limit, - name, - slug, - sortBy, - sortOrder, - }) => { - try { - const response = await getClient().collections.items.listItems( - collection_id, - { - cmsLocaleId, - offset, - limit, - name, - slug, - sortBy, - sortOrder, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); + }).optional().describe("Filter and sort items in a CMS collection."), + }).optional().describe("List items in a CMS collection with optional filtering and sorting."), + // POST https://api.webflow.com/v2/collections/:collection_id/items/bulk + create_collection_items:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + request: z.object({ + cmsLocaleIds:z.array(z.string()).optional().describe("Unique identifier for the locale of the CMS Item."), + isArchived:z.boolean().optional().describe("Indicates if the item is archived."), + isDraft:z.boolean().optional().describe("Indicates if the item is a draft."), + fieldData:z.array(z.record(z.any()).and(z.object({ + name:z.string().describe("Name of the field."), + slug:z.string().describe("URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug."), + }))).describe("Data of the item."), + }).describe("Array of items to be created."), + }).optional().describe("Create new items in a CMS collection as drafts."), + //PATCH https://api.webflow.com/v2/collections/:collection_id/items + update_collection_items:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + request: WebflowCollectionsItemsUpdateItemsRequestSchema.describe("Array of items to be updated."), + }).optional().describe("Update existing items in a CMS collection as drafts."), + // POST https://api.webflow.com/v2/collections/:collection_id/items/publish + publish_collection_items:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + request: z.object({ + itemIds: z.array(z.string()).describe("Array of item IDs to be published."), + }).describe("Array of items to be published."), + }).optional().describe("Publish existing items in a CMS collection as drafts."), + // DEL https://api.webflow.com/v2/collections/:collection_id/items + delete_collection_items:z.object({ + collection_id: z.string().describe("Unique identifier for the Collection."), + request: z.object({ + items: z.array(z.object({ + id: z.string().describe("Item ID to be deleted."), + cmsLocaleIds: z.array(z.string()).optional().describe("Unique identifier for the locale of the CMS Item."), + })).describe("Array of items to be deleted."), + }).describe("Array of items to be deleted."), + }).optional().describe("Delete existing items in a CMS collection as drafts."), + }) + ) + },async ({actions})=>{ + const result : Content[] = []; + try{ + for(const action of actions){ + if(action.get_collection_list){ + const content = await getCollectionList(action.get_collection_list); + result.push(textContent(content)); + }else if(action.get_collection_details){ + const content = await getCollectionDetails(action.get_collection_details); + result.push(textContent(content)); + }else if(action.create_collection){ + const content = await createCollection(action.create_collection); + result.push(textContent(content)); + }else if(action.create_collection_static_field){ + const content = await createCollectionStaticField(action.create_collection_static_field); + result.push(textContent(content)); + }else if(action.create_collection_option_field){ + const content = await createCollectionOptionField(action.create_collection_option_field); + result.push(textContent(content)); + }else if(action.create_collection_reference_field){ + const content = await createCollectionReferenceField(action.create_collection_reference_field); + result.push(textContent(content)); } - } - ); - - // POST https://api.webflow.com/v2/collections/:collection_id/items - server.tool( - "collections_items_create_item", - "Create new items in a CMS collection as drafts.", - { - collection_id: z.string(), - request: WebflowCollectionsItemsCreateItemRequestSchema, - }, - async ({ collection_id, request }) => { - try { - const response = await getClient().collections.items.createItem( - collection_id, - request, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); - - // PATCH https://api.webflow.com/v2/collections/:collection_id/items - server.tool( - "collections_items_update_items", - "Update existing items in a CMS collection as drafts.", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: WebflowCollectionsItemsUpdateItemsRequestSchema, - }, - async ({ collection_id, request }) => { - try { - const response = await getClient().collections.items.updateItems( - collection_id, - request, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); + else if(action.update_collection_field){ + const content = await updateCollectionField(action.update_collection_field); + result.push(textContent(content)); } - } - ); - - // POST https://api.webflow.com/v2/collections/:collection_id/items/publish - server.tool( - "collections_items_publish_items", - "Publish draft items in a CMS collection to make them live.", - { - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - itemIds: z - .array(z.string()) - .describe("Array of item IDs to be published."), - }, - async ({ collection_id, itemIds }) => { - try { - const response = await getClient().collections.items.publishItem( - collection_id, - { - itemIds: itemIds, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); + // else if(action.create_collection_items_live){ + // const content = await createCollectionItemsLive(action.create_collection_items_live); + // result.push(textContent(content)); + // } + // else if(action.update_collection_items_live){ + // const content = await updateCollectionItemsLive(action.update_collection_items_live); + // result.push(textContent(content)); + // } + else if(action.list_collection_items){ + const content = await listCollectionItems({ + collection_id: action.list_collection_items.collection_id, + request: action.list_collection_items.request || {}, + }); + result.push(textContent(content)); + }else if(action.create_collection_items){ + const content = await createCollectionItems({ + collection_id: action.create_collection_items.collection_id, + request: action.create_collection_items.request, + }); + result.push(textContent(content)); + }else if(action.update_collection_items){ + const content = await updateCollectionItems({ + collection_id: action.update_collection_items.collection_id, + request: action.update_collection_items.request, + }); + result.push(textContent(content)); + }else if(action.publish_collection_items){ + const content = await publishCollectionItems({ + collection_id: action.publish_collection_items.collection_id, + request: action.publish_collection_items.request, + }); + result.push(textContent(content)); + }else if(action.delete_collection_items){ + const content = await deleteCollectionItems({ + collection_id: action.delete_collection_items.collection_id, + request: action.delete_collection_items.request, + }); + result.push(textContent(content)); } } - ); - - - // DEL https://api.webflow.com/v2/collections/:collection_id/items/ - server.tool( - "collections_items_delete_item", - "Delete an item in a CMS collection. Items will only be deleted in the primary locale unless a cmsLocaleId is included in the request. ", - { - collection_id: z.string().describe("Unique identifier for the Collection."), - itemId: z.string().describe("Item ID to be deleted."), - cmsLocaleIds: z.string().optional().describe("Unique identifier for the locale of the CMS Item."), - }, - async ({ collection_id, itemId, cmsLocaleIds }) => { - try { - const response = await getClient().collections.items.deleteItem( - collection_id, - itemId, - { cmsLocaleId: cmsLocaleIds}, - requestOptions - ); - return formatResponse(JSON.stringify("Item deleted")); + return toolResponse(result); } catch (error) { return formatErrorResponse(error); } - } - ); + }); + + } From 5d0a4454f18332fdc876c190bfc9e1c3fe698e31 Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Wed, 1 Oct 2025 02:24:16 +0530 Subject: [PATCH 03/14] Sites tool consolidation --- src/tools/sites.ts | 133 +++++++++++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 54 deletions(-) diff --git a/src/tools/sites.ts b/src/tools/sites.ts index 68df527..73d4142 100644 --- a/src/tools/sites.ts +++ b/src/tools/sites.ts @@ -2,71 +2,96 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WebflowClient } from "webflow-api"; import { z } from "zod"; import { requestOptions } from "../mcp"; -import { formatErrorResponse, formatResponse } from "../utils"; +import { Content, formatErrorResponse, textContent, toolResponse } from "../utils"; export function registerSiteTools( server: McpServer, getClient: () => WebflowClient ) { - // GET https://api.webflow.com/v2/sites - server.tool( - "sites_list", - "List all sites accessible to the authenticated user. Returns basic site information including site ID, name, and last published date.", - async () => { - try { - const response = await getClient().sites.list(requestOptions); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const listSites = async () => { + const response = await getClient().sites.list(requestOptions); + return response; + }; - // GET https://api.webflow.com/v2/sites/:site_id - server.tool( - "sites_get", - "Get detailed information about a specific site including its settings, domains, and publishing status.", - { - site_id: z.string().describe("Unique identifier for the site."), - }, - async ({ site_id }) => { - try { - const response = await getClient().sites.get(site_id, requestOptions); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const getSite = async (arg: { site_id: string }) => { + const response = await getClient().sites.get(arg.site_id, requestOptions); + return response; + }; + + const publishSite = async (arg: { + site_id: string; + customDomains?: string[]; + publishToWebflowSubdomain?: boolean; + }) => { + const response = await getClient().sites.publish( + arg.site_id, + { + customDomains: arg.customDomains, + publishToWebflowSubdomain: arg.publishToWebflowSubdomain, + }, + requestOptions + ); + return response; + }; - // POST https://api.webflow.com/v2/sites/:site_id/publish server.tool( - "sites_publish", - "Publish a site to specified domains. This will make the latest changes live on the specified domains.", + "data_sites_tool", + "Data tool - Sites tool to perform actions like list sites, get site details, and publish sites", { - site_id: z.string().describe("Unique identifier for the site."), - customDomains: z - .string() - .array() - .optional() - .describe("Array of custom domains to publish the site to."), - publishToWebflowSubdomain: z - .boolean() - .optional() - .default(false) - .describe("Whether to publish to the Webflow subdomain."), + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites + list_sites: z + .object({}) + .optional() + .describe( + "List all sites accessible to the authenticated user. Returns basic site information including site ID, name, and last published date." + ), + // GET https://api.webflow.com/v2/sites/:site_id + get_site: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + }) + .optional() + .describe( + "Get detailed information about a specific site including its settings, domains, and publishing status." + ), + // POST https://api.webflow.com/v2/sites/:site_id/publish + publish_site: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + customDomains: z + .array(z.string()) + .optional() + .describe("Array of custom domains to publish the site to."), + publishToWebflowSubdomain: z + .boolean() + .optional() + .describe("Whether to publish to the Webflow subdomain."), + }) + .optional() + .describe( + "Publish a site to specified domains. This will make the latest changes live on the specified domains." + ), + }) + ), }, - async ({ site_id, customDomains, publishToWebflowSubdomain }) => { + async ({ actions }) => { + const result: Content[] = []; try { - const response = await getClient().sites.publish( - site_id, - { - customDomains, - publishToWebflowSubdomain, - }, - requestOptions - ); - return formatResponse(response); + for (const action of actions) { + if (action.list_sites) { + const content = await listSites(); + result.push(textContent(content)); + } else if (action.get_site) { + const content = await getSite(action.get_site); + result.push(textContent(content)); + } else if (action.publish_site) { + const content = await publishSite(action.publish_site); + result.push(textContent(content)); + } + } + return toolResponse(result); } catch (error) { return formatErrorResponse(error); } From f7ac21853161620f963f15bf94d169181d65ac69 Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Wed, 1 Oct 2025 02:28:04 +0530 Subject: [PATCH 04/14] Script tool consolidation --- src/tools/scripts.ts | 250 ++++++++++++++++++++++++++----------------- 1 file changed, 149 insertions(+), 101 deletions(-) diff --git a/src/tools/scripts.ts b/src/tools/scripts.ts index a40699b..d888bc9 100644 --- a/src/tools/scripts.ts +++ b/src/tools/scripts.ts @@ -4,128 +4,176 @@ import { ScriptApplyLocation } from "webflow-api/api/types/ScriptApplyLocation"; import { z } from "zod"; import { requestOptions } from "../mcp"; import { RegisterInlineSiteScriptSchema } from "../schemas"; -import { formatErrorResponse, formatResponse, isApiError } from "../utils"; +import { Content, formatErrorResponse, textContent, toolResponse, isApiError } from "../utils"; export function registerScriptsTools( server: McpServer, getClient: () => WebflowClient ) { - // GET https://api.webflow.com/v2/sites/:site_id/registered_scripts - server.tool( - "site_registered_scripts_list", - "List all registered scripts for a site. To apply a script to a site or page, first register it via the Register Script endpoints, then apply it using the relevant Site or Page endpoints.", - { - site_id: z.string().describe("Unique identifier for the site."), - }, - async ({ site_id }) => { - try { - const response = await getClient().scripts.list( - site_id, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const listRegisteredScripts = async (arg: { site_id: string }) => { + const response = await getClient().scripts.list( + arg.site_id, + requestOptions + ); + return response; + }; - // GET https://api.webflow.com/v2/sites/:site_id/custom_code - server.tool( - "site_applied_scripts_list", - "Get all scripts applied to a site by the App. To apply a script to a site or page, first register it via the Register Script endpoints, then apply it using the relevant Site or Page endpoints.", - { - site_id: z.string().describe("Unique identifier for the site."), - }, - async ({ site_id }) => { - try { - const response = await getClient().sites.scripts.getCustomCode( - site_id, + const listAppliedScripts = async (arg: { site_id: string }) => { + const response = await getClient().sites.scripts.getCustomCode( + arg.site_id, + requestOptions + ); + return response; + }; + + const addInlineSiteScript = async (arg: { + site_id: string; + request: { + sourceCode: string; + version: string; + displayName: string; + location?: string; + canCopy?: boolean; + attributes?: Record; + }; + }) => { + const registerScriptResponse = await getClient().scripts.registerInline( + arg.site_id, + { + sourceCode: arg.request.sourceCode, + version: arg.request.version, + displayName: arg.request.displayName, + canCopy: arg.request.canCopy !== undefined ? arg.request.canCopy : true, + }, + requestOptions + ); + + let existingScripts: any[] = []; + try { + const allScriptsResponse = + await getClient().sites.scripts.getCustomCode( + arg.site_id, requestOptions ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } + existingScripts = allScriptsResponse.scripts || []; + } catch (error) { + existingScripts = []; } - ); - // POST https://api.webflow.com/v2/sites/:site_id/registered_scripts/inline - server.tool( - "add_inline_site_script", - "Register an inline script for a site. Inline scripts are limited to 2000 characters. ", - { - site_id: z.string().describe("Unique identifier for the site."), - request: RegisterInlineSiteScriptSchema, - }, - async ({ site_id, request }) => { - const registerScriptResponse = await getClient().scripts.registerInline( - site_id, - { - sourceCode: request.sourceCode, - version: request.version, - displayName: request.displayName, - canCopy: request.canCopy !== undefined ? request.canCopy : true, - }, - requestOptions - ); + const newScript = { + id: registerScriptResponse.id ?? " ", + location: + arg.request.location === "footer" + ? ScriptApplyLocation.Footer + : ScriptApplyLocation.Header, + version: registerScriptResponse.version ?? " ", + attributes: arg.request.attributes, + }; - let existingScripts: any[] = []; - try { - const allScriptsResponse = - await getClient().sites.scripts.getCustomCode( - site_id, - requestOptions - ); - existingScripts = allScriptsResponse.scripts || []; - } catch (error) { - formatErrorResponse(error); - existingScripts = []; - } + existingScripts.push(newScript); - const newScript = { - id: registerScriptResponse.id ?? " ", - location: - request.location === "footer" - ? ScriptApplyLocation.Footer - : ScriptApplyLocation.Header, - version: registerScriptResponse.version ?? " ", - attributes: request.attributes, - }; + await getClient().sites.scripts.upsertCustomCode( + arg.site_id, + { + scripts: existingScripts, + }, + requestOptions + ); - existingScripts.push(newScript); - - const addedSiteCustomCoderesponse = - await getClient().sites.scripts.upsertCustomCode( - site_id, - { - scripts: existingScripts, - }, - requestOptions - ); + return registerScriptResponse; + }; - return formatResponse(registerScriptResponse); + const deleteAllSiteScripts = async (arg: { site_id: string }) => { + try { + await getClient().sites.scripts.deleteCustomCode( + arg.site_id, + requestOptions + ); + return "Custom Code Deleted"; + } catch (error) { + // If it's a 404, we'll try to clear the scripts another way + if (isApiError(error) && error.status === 404) { + return error.message ?? "No custom code found"; + } + throw error; } - ); + }; server.tool( - "delete_all_site_scripts", + "data_scripts_tool", + "Data tool - Scripts tool to perform actions like list registered scripts, list applied scripts, add inline site script, and delete all site scripts", { - site_id: z.string(), + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites/:site_id/registered_scripts + list_registered_scripts: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + }) + .optional() + .describe( + "List all registered scripts for a site. To apply a script to a site or page, first register it via the Register Script endpoints, then apply it using the relevant Site or Page endpoints." + ), + // GET https://api.webflow.com/v2/sites/:site_id/custom_code + list_applied_scripts: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + }) + .optional() + .describe( + "Get all scripts applied to a site by the App. To apply a script to a site or page, first register it via the Register Script endpoints, then apply it using the relevant Site or Page endpoints." + ), + // POST https://api.webflow.com/v2/sites/:site_id/registered_scripts/inline + add_inline_site_script: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + request: RegisterInlineSiteScriptSchema, + }) + .optional() + .describe( + "Register an inline script for a site. Inline scripts are limited to 2000 characters." + ), + // DELETE https://api.webflow.com/v2/sites/:site_id/custom_code + delete_all_site_scripts: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + }) + .optional() + .describe( + "Delete all custom scripts applied to a site by the App." + ), + }) + ), }, - async ({ site_id }) => { + async ({ actions }) => { + const result: Content[] = []; try { - const response = await getClient().sites.scripts.deleteCustomCode( - site_id, - requestOptions - ); - return formatResponse("Custom Code Deleted"); - } catch (error) { - // If it's a 404, we'll try to clear the scripts another way - if (isApiError(error) && error.status === 404) { - return formatResponse(error.message ?? "No custom code found"); + for (const action of actions) { + if (action.list_registered_scripts) { + const content = await listRegisteredScripts( + action.list_registered_scripts + ); + result.push(textContent(content)); + } else if (action.list_applied_scripts) { + const content = await listAppliedScripts( + action.list_applied_scripts + ); + result.push(textContent(content)); + } else if (action.add_inline_site_script) { + const content = await addInlineSiteScript( + action.add_inline_site_script + ); + result.push(textContent(content)); + } else if (action.delete_all_site_scripts) { + const content = await deleteAllSiteScripts( + action.delete_all_site_scripts + ); + result.push(textContent(content)); + } } - throw error; + return toolResponse(result); + } catch (error) { + return formatErrorResponse(error); } } ); From e4c21ca8b63f08cd8a93abb673d09799d528971a Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Wed, 1 Oct 2025 02:32:06 +0530 Subject: [PATCH 05/14] Data Page tool consolidation --- src/tools/pages.ts | 369 +++++++++++++++++++++++++-------------------- 1 file changed, 208 insertions(+), 161 deletions(-) diff --git a/src/tools/pages.ts b/src/tools/pages.ts index 1daf549..e4613b6 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -6,180 +6,227 @@ import { WebflowPageDomWriteNodesItemSchema, WebflowPageSchema, } from "../schemas"; -import { formatErrorResponse, formatResponse } from "../utils"; +import { Content, formatErrorResponse, textContent, toolResponse } from "../utils"; export function registerPagesTools( server: McpServer, getClient: () => WebflowClient ) { - // GET https://api.webflow.com/v2/sites/:site_id/pages - server.tool( - "pages_list", - "List all pages within a site. Returns page metadata including IDs, titles, and slugs.", - { - site_id: z - .string() - .describe("The site’s unique ID, used to list its pages."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - limit: z - .number() - .optional() - .describe("Maximum number of records to be returned (max limit: 100)"), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }, - async ({ site_id, localeId, limit, offset }) => { - try { - const response = await getClient().pages.list( - site_id, - { - localeId, - limit, - offset, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const listPages = async (arg: { + site_id: string; + localeId?: string; + limit?: number; + offset?: number; + }) => { + const response = await getClient().pages.list( + arg.site_id, + { + localeId: arg.localeId, + limit: arg.limit, + offset: arg.offset, + }, + requestOptions + ); + return response; + }; - // GET https://api.webflow.com/v2/pages/:page_id - server.tool( - "pages_get_metadata", - "Get metadata for a specific page including SEO settings, Open Graph data, and page status (draft/published).", - { - page_id: z.string().describe("Unique identifier for the page."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - }, - async ({ page_id, localeId }) => { - try { - const response = await getClient().pages.getMetadata( - page_id, - { - localeId, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const getPageMetadata = async (arg: { + page_id: string; + localeId?: string; + }) => { + const response = await getClient().pages.getMetadata( + arg.page_id, + { + localeId: arg.localeId, + }, + requestOptions + ); + return response; + }; - // PUT https://api.webflow.com/v2/pages/:page_id - server.tool( - "pages_update_page_settings", - "Update page settings including SEO metadata, Open Graph data, slug, and publishing status.", - { - page_id: z.string().describe("Unique identifier for the page."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - body: WebflowPageSchema, - }, - async ({ page_id, localeId, body }) => { - try { - const response = await getClient().pages.updatePageSettings( - page_id, - { - localeId, - body, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const updatePageSettings = async (arg: { + page_id: string; + localeId?: string; + body: any; + }) => { + const response = await getClient().pages.updatePageSettings( + arg.page_id, + { + localeId: arg.localeId, + body: arg.body, + }, + requestOptions + ); + return response; + }; - // GET https://api.webflow.com/v2/pages/:page_id/dom - server.tool( - "pages_get_content", - "Get the content structure and data for a specific page including all elements and their properties.", - { - page_id: z.string().describe("Unique identifier for the page."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - limit: z - .number() - .optional() - .describe("Maximum number of records to be returned (max limit: 100)"), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }, - async ({ page_id, localeId, limit, offset }) => { - try { - const response = await getClient().pages.getContent( - page_id, - { - localeId, - limit, - offset, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const getPageContent = async (arg: { + page_id: string; + localeId?: string; + limit?: number; + offset?: number; + }) => { + const response = await getClient().pages.getContent( + arg.page_id, + { + localeId: arg.localeId, + limit: arg.limit, + offset: arg.offset, + }, + requestOptions + ); + return response; + }; + + const updateStaticContent = async (arg: { + page_id: string; + localeId: string; + nodes: any; + }) => { + const response = await getClient().pages.updateStaticContent( + arg.page_id, + { + localeId: arg.localeId, + nodes: arg.nodes, + }, + requestOptions + ); + return response; + }; - // POST https://api.webflow.com/v2/pages/:page_id/dom server.tool( - "pages_update_static_content", - "Update content on a static page in secondary locales by modifying text nodes and property overrides.", + "data_pages_tool", + "Data tool - Pages tool to perform actions like list pages, get page metadata, update page settings, get page content, and update static content", { - page_id: z.string().describe("Unique identifier for the page."), - localeId: z - .string() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - nodes: WebflowPageDomWriteNodesItemSchema, + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites/:site_id/pages + list_pages: z + .object({ + site_id: z + .string() + .describe("The site's unique ID, used to list its pages."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "List all pages within a site. Returns page metadata including IDs, titles, and slugs." + ), + // GET https://api.webflow.com/v2/pages/:page_id + get_page_metadata: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + }) + .optional() + .describe( + "Get metadata for a specific page including SEO settings, Open Graph data, and page status (draft/published)." + ), + // PUT https://api.webflow.com/v2/pages/:page_id + update_page_settings: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + body: WebflowPageSchema, + }) + .optional() + .describe( + "Update page settings including SEO metadata, Open Graph data, slug, and publishing status." + ), + // GET https://api.webflow.com/v2/pages/:page_id/dom + get_page_content: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "Get the content structure and data for a specific page including all elements and their properties for localization." + ), + // POST https://api.webflow.com/v2/pages/:page_id/dom + update_static_content: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + localeId: z + .string() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + nodes: WebflowPageDomWriteNodesItemSchema, + }) + .optional() + .describe( + "Update content on a static page in secondary locales by modifying text nodes and property overrides." + ), + }) + ), }, - async ({ page_id, localeId, nodes }) => { + async ({ actions }) => { + const result: Content[] = []; try { - const response = await getClient().pages.updateStaticContent( - page_id, - { - localeId, - nodes, - }, - requestOptions - ); - return formatResponse(response); + for (const action of actions) { + if (action.list_pages) { + const content = await listPages(action.list_pages); + result.push(textContent(content)); + } else if (action.get_page_metadata) { + const content = await getPageMetadata(action.get_page_metadata); + result.push(textContent(content)); + } else if (action.update_page_settings) { + const content = await updatePageSettings(action.update_page_settings); + result.push(textContent(content)); + } else if (action.get_page_content) { + const content = await getPageContent(action.get_page_content); + result.push(textContent(content)); + } else if (action.update_static_content) { + const content = await updateStaticContent(action.update_static_content); + result.push(textContent(content)); + } + } + return toolResponse(result); } catch (error) { return formatErrorResponse(error); } From fef7bccc969fefc77f02d6de56eebabec3757c9c Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Wed, 1 Oct 2025 02:36:30 +0530 Subject: [PATCH 06/14] Data Component tool consolidation --- src/tools/components.ts | 412 +++++++++++++++++++++++----------------- 1 file changed, 241 insertions(+), 171 deletions(-) diff --git a/src/tools/components.ts b/src/tools/components.ts index 76ff2f5..ff5a15c 100644 --- a/src/tools/components.ts +++ b/src/tools/components.ts @@ -6,190 +6,260 @@ import { ComponentDomWriteNodesItemSchema, ComponentPropertyUpdateSchema } from "../schemas"; -import { formatErrorResponse, formatResponse } from "../utils"; +import { Content, formatErrorResponse, textContent, toolResponse } from "../utils"; export function registerComponentsTools( server: McpServer, getClient: () => WebflowClient ) { - // GET https://api.webflow.com/v2/sites/:site_id/components - server.tool( - "components_list", - "List all components in a site. Returns component metadata including IDs, names, and versions.", - { - site_id: z.string().describe("Unique identifier for the Site."), - limit: z - .number() - .optional() - .describe("Maximum number of records to be returned (max limit: 100)"), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }, - async ({ site_id, limit, offset }) => { - try { - const response = await getClient().components.list( - site_id, - { - limit, - offset, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const listComponents = async (arg: { + site_id: string; + limit?: number; + offset?: number; + }) => { + const response = await getClient().components.list( + arg.site_id, + { + limit: arg.limit, + offset: arg.offset, + }, + requestOptions + ); + return response; + }; - // GET https://api.webflow.com/v2/sites/:site_id/components/:component_id/dom - server.tool( - "components_get_content", - "Get the content structure and data for a specific component including text, images, and nested components.", - { - site_id: z.string().describe("Unique identifier for the Site."), - component_id: z.string().describe("Unique identifier for the Component."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - limit: z - .number() - .optional() - .describe("Maximum number of records to be returned (max limit: 100)"), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }, - async ({ site_id, component_id, localeId, limit, offset }) => { - try { - const response = await getClient().components.getContent( - site_id, - component_id, - { - localeId, - limit, - offset, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const getComponentContent = async (arg: { + site_id: string; + component_id: string; + localeId?: string; + limit?: number; + offset?: number; + }) => { + const response = await getClient().components.getContent( + arg.site_id, + arg.component_id, + { + localeId: arg.localeId, + limit: arg.limit, + offset: arg.offset, + }, + requestOptions + ); + return response; + }; - // POST https://api.webflow.com/v2/sites/:site_id/components/:component_id/dom - server.tool( - "components_update_content", - "Update content on a component in secondary locales by modifying text nodes and property overrides.", - { - site_id: z.string().describe("Unique identifier for the Site."), - component_id: z.string().describe("Unique identifier for the Component."), - localeId: z - .string() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - nodes: ComponentDomWriteNodesItemSchema, - }, - async ({ site_id, component_id, localeId, nodes }) => { - try { - const response = await getClient().components.updateContent( - site_id, - component_id, - { - localeId, - nodes, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const updateComponentContent = async (arg: { + site_id: string; + component_id: string; + localeId: string; + nodes: any; + }) => { + const response = await getClient().components.updateContent( + arg.site_id, + arg.component_id, + { + localeId: arg.localeId, + nodes: arg.nodes, + }, + requestOptions + ); + return response; + }; - // GET https://api.webflow.com/v2/sites/:site_id/components/:component_id/properties - server.tool( - "components_get_properties", - "Get component properties including default values and configuration for a specific component.", - { - site_id: z.string().describe("Unique identifier for the Site."), - component_id: z.string().describe("Unique identifier for the Component."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - limit: z - .number() - .optional() - .describe("Maximum number of records to be returned (max limit: 100)"), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }, - async ({ site_id, component_id, localeId, limit, offset }) => { - try { - const response = await getClient().components.getProperties( - site_id, - component_id, - { - localeId, - limit, - offset, - }, - requestOptions - ); - return formatResponse(response); - } catch (error) { - return formatErrorResponse(error); - } - } - ); + const getComponentProperties = async (arg: { + site_id: string; + component_id: string; + localeId?: string; + limit?: number; + offset?: number; + }) => { + const response = await getClient().components.getProperties( + arg.site_id, + arg.component_id, + { + localeId: arg.localeId, + limit: arg.limit, + offset: arg.offset, + }, + requestOptions + ); + return response; + }; + + const updateComponentProperties = async (arg: { + site_id: string; + component_id: string; + localeId: string; + properties: any; + }) => { + const response = await getClient().components.updateProperties( + arg.site_id, + arg.component_id, + { + localeId: arg.localeId, + properties: arg.properties, + }, + requestOptions + ); + return response; + }; - // POST https://api.webflow.com/v2/sites/:site_id/components/:component_id/properties server.tool( - "components_update_properties", - "Update component properties for localization to customize behavior in different languages.", + "data_components_tool", + "Data tool - Components tool to perform actions like list components, get component content, update component content, get component properties, and update component properties", { - site_id: z.string().describe("Unique identifier for the Site."), - component_id: z.string().describe("Unique identifier for the Component."), - localeId: z - .string() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - properties: ComponentPropertyUpdateSchema, + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites/:site_id/components + list_components: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "List all components in a site. Returns component metadata including IDs, names, and versions." + ), + // GET https://api.webflow.com/v2/sites/:site_id/components/:component_id/dom + get_component_content: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + component_id: z + .string() + .describe("Unique identifier for the Component."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "Get the content structure and data for a specific component including text, images, and nested components." + ), + // POST https://api.webflow.com/v2/sites/:site_id/components/:component_id/dom + update_component_content: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + component_id: z + .string() + .describe("Unique identifier for the Component."), + localeId: z + .string() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + nodes: ComponentDomWriteNodesItemSchema, + }) + .optional() + .describe( + "Update content on a component in secondary locales by modifying text nodes and property overrides." + ), + // GET https://api.webflow.com/v2/sites/:site_id/components/:component_id/properties + get_component_properties: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + component_id: z + .string() + .describe("Unique identifier for the Component."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "Get component properties including default values and configuration for a specific component." + ), + // POST https://api.webflow.com/v2/sites/:site_id/components/:component_id/properties + update_component_properties: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + component_id: z + .string() + .describe("Unique identifier for the Component."), + localeId: z + .string() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + properties: ComponentPropertyUpdateSchema, + }) + .optional() + .describe( + "Update component properties for localization to customize behavior in different languages." + ), + }) + ), }, - async ({ site_id, component_id, localeId, properties }) => { + async ({ actions }) => { + const result: Content[] = []; try { - const response = await getClient().components.updateProperties( - site_id, - component_id, - { - localeId, - properties, - }, - requestOptions - ); - return formatResponse(response); + for (const action of actions) { + if (action.list_components) { + const content = await listComponents(action.list_components); + result.push(textContent(content)); + } else if (action.get_component_content) { + const content = await getComponentContent( + action.get_component_content + ); + result.push(textContent(content)); + } else if (action.update_component_content) { + const content = await updateComponentContent( + action.update_component_content + ); + result.push(textContent(content)); + } else if (action.get_component_properties) { + const content = await getComponentProperties( + action.get_component_properties + ); + result.push(textContent(content)); + } else if (action.update_component_properties) { + const content = await updateComponentProperties( + action.update_component_properties + ); + result.push(textContent(content)); + } + } + return toolResponse(result); } catch (error) { return formatErrorResponse(error); } From 81a368c8eeb5020db239c03199e36ca5c64f9a3c Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Wed, 1 Oct 2025 02:36:48 +0530 Subject: [PATCH 07/14] fixed cms tool description --- src/tools/cms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/cms.ts b/src/tools/cms.ts index 205efb3..a34da94 100644 --- a/src/tools/cms.ts +++ b/src/tools/cms.ts @@ -167,7 +167,7 @@ export function registerCmsTools( return response; } - server.tool("data_cms_tool","Data tool - CMS tool to perform actions like get collection list, get collection details, create collection, create collection static field, create collection option field, create collection reference field, update collection field, create collection item live, update collection items live, list collection items, create collection item, update collection items, publish collection items, delete collection item",{ + server.tool("data_cms_tool","Data tool - CMS tool to perform actions like get collection list, get collection details, create collection, create collection fields (static/option/reference), update collection field, list collection items, create collection items, update collection items, publish collection items, and delete collection items",{ actions:z.array( z.object({ // GET https://api.webflow.com/v2/sites/:site_id/collections From 791097ca3044a959e4414bcf22c98630680fb218 Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Fri, 17 Oct 2025 20:51:26 +0530 Subject: [PATCH 08/14] Refactor CMS tool for improved type safety and consistency Updated the CMS tool to enhance TypeScript type definitions for function arguments and responses, ensuring better clarity and maintainability. Consolidated import statements and improved formatting for better readability. This refactor aligns with recent updates across other tools for a unified codebase. --- src/tools/cms.ts | 633 ++++++++++++++++++++++++++-------------- src/tools/components.ts | 21 +- src/tools/pages.ts | 27 +- src/tools/scripts.ts | 26 +- src/tools/sites.ts | 13 +- 5 files changed, 469 insertions(+), 251 deletions(-) diff --git a/src/tools/cms.ts b/src/tools/cms.ts index a34da94..9e497b0 100644 --- a/src/tools/cms.ts +++ b/src/tools/cms.ts @@ -15,70 +15,94 @@ import { // WebflowCollectionsItemsUpdateItemsLiveRequestSchema, WebflowCollectionsItemsUpdateItemsRequestSchema, } from "../schemas"; -import { Content, formatErrorResponse, formatResponse, textContent, toolResponse } from "../utils"; +import { + Content, + formatErrorResponse, + formatResponse, + textContent, + toolResponse, +} from "../utils"; import { CollectionsCreateRequest, FieldCreate } from "webflow-api/api"; import { FieldUpdate } from "webflow-api/api/resources/collections/resources/fields"; -import { +import { //ItemsCreateItemLiveRequest, //ItemsUpdateItemsLiveRequest, - ItemsDeleteItemsRequest, ItemsListItemsRequest, - ItemsUpdateItemsRequest } from "webflow-api/api/resources/collections/resources/items"; + ItemsDeleteItemsRequest, + ItemsListItemsRequest, + ItemsUpdateItemsRequest, +} from "webflow-api/api/resources/collections/resources/items"; export function registerCmsTools( server: McpServer, getClient: () => WebflowClient ) { - const getCollectionList = async (arg:{siteId:string})=>{ + const getCollectionList = async (arg: { siteId: string }) => { const response = await getClient().collections.list( arg.siteId, requestOptions ); return response; - } - const getCollectionDetails = async (arg:{collection_id:string})=>{ + }; + const getCollectionDetails = async (arg: { collection_id: string }) => { const response = await getClient().collections.get( arg.collection_id, requestOptions ); return response; - } + }; - const createCollection = async (arg:{siteId:string, request: CollectionsCreateRequest})=>{ + const createCollection = async (arg: { + siteId: string; + request: CollectionsCreateRequest; + }) => { const response = await getClient().collections.create( arg.siteId, arg.request, requestOptions ); return response; - } + }; - const createCollectionStaticField = async (arg:{collection_id:string, request: FieldCreate})=>{ + const createCollectionStaticField = async (arg: { + collection_id: string; + request: FieldCreate; + }) => { const response = await getClient().collections.fields.create( arg.collection_id, arg.request, requestOptions ); return response; - } + }; - const createCollectionOptionField = async (arg:{collection_id:string, request: FieldCreate})=>{ + const createCollectionOptionField = async (arg: { + collection_id: string; + request: FieldCreate; + }) => { const response = await getClient().collections.fields.create( arg.collection_id, arg.request, requestOptions ); return response; - } - const createCollectionReferenceField = async (arg:{collection_id:string, request: FieldCreate})=>{ + }; + const createCollectionReferenceField = async (arg: { + collection_id: string; + request: FieldCreate; + }) => { const response = await getClient().collections.fields.create( arg.collection_id, arg.request, requestOptions ); return response; - } + }; - const updateCollectionField = async (arg:{collection_id:string, field_id:string, request: FieldUpdate})=>{ + const updateCollectionField = async (arg: { + collection_id: string; + field_id: string; + request: FieldUpdate; + }) => { const response = await getClient().collections.fields.update( arg.collection_id, arg.field_id, @@ -86,7 +110,7 @@ export function registerCmsTools( requestOptions ); return response; - } + }; // const createCollectionItemsLive = async (arg:{collection_id:string, request: ItemsCreateItemLiveRequest})=>{ // const response = await getClient().collections.items.createItemLive( @@ -105,26 +129,31 @@ export function registerCmsTools( // return response; // } - const listCollectionItems = async (arg:{collection_id:string, request: ItemsListItemsRequest})=>{ + const listCollectionItems = async (arg: { + collection_id: string; + request: ItemsListItemsRequest; + }) => { const response = await getClient().collections.items.listItems( arg.collection_id, arg.request, requestOptions ); return response; - } - - const createCollectionItems = async (arg:{collection_id:string, request: { - cmsLocaleIds?: string[]; - isArchived?: boolean; - isDraft?: boolean; - fieldData: { - name: string; - slug: string; - [key: string]: any; - }[]; - }})=>{ + }; + const createCollectionItems = async (arg: { + collection_id: string; + request: { + cmsLocaleIds?: string[]; + isArchived?: boolean; + isDraft?: boolean; + fieldData: { + name: string; + slug: string; + [key: string]: any; + }[]; + }; + }) => { const response = await getClient().collections.items.createItems( arg.collection_id, { @@ -136,19 +165,25 @@ export function registerCmsTools( requestOptions ); return response; - } + }; - const updateCollectionItems = async (arg:{collection_id:string, request: ItemsUpdateItemsRequest})=>{ + const updateCollectionItems = async (arg: { + collection_id: string; + request: ItemsUpdateItemsRequest; + }) => { const response = await getClient().collections.items.updateItems( arg.collection_id, arg.request, requestOptions ); return response; - } - const publishCollectionItems = async (arg:{collection_id:string, request: { - itemIds: string[]; - }})=>{ + }; + const publishCollectionItems = async (arg: { + collection_id: string; + request: { + itemIds: string[]; + }; + }) => { const response = await getClient().collections.items.publishItem( arg.collection_id, { @@ -157,202 +192,348 @@ export function registerCmsTools( requestOptions ); return response; - } - const deleteCollectionItems = async (arg:{collection_id:string, request: ItemsDeleteItemsRequest})=>{ + }; + const deleteCollectionItems = async (arg: { + collection_id: string; + request: ItemsDeleteItemsRequest; + }) => { const response = await getClient().collections.items.deleteItems( arg.collection_id, arg.request, requestOptions ); return response; - } + }; - server.tool("data_cms_tool","Data tool - CMS tool to perform actions like get collection list, get collection details, create collection, create collection fields (static/option/reference), update collection field, list collection items, create collection items, update collection items, publish collection items, and delete collection items",{ - actions:z.array( - z.object({ - // GET https://api.webflow.com/v2/sites/:site_id/collections - get_collection_list:z.object({ - ...SiteIdSchema, - }).optional().describe("List all CMS collections in a site. Returns collection metadata including IDs, names, and schemas."), - // GET https://api.webflow.com/v2/collections/:collection_id - get_collection_details:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - }).optional().describe("Get detailed information about a specific CMS collection including its schema and field definitions."), - // POST https://api.webflow.com/v2/sites/:site_id/collections - create_collection:z.object({ - ...SiteIdSchema, - request: WebflowCollectionsCreateRequestSchema, - }).optional().describe("Create a new CMS collection in a site with specified name and schema."), - // POST https://api.webflow.com/v2/collections/:collection_id/fields - create_collection_static_field:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - request: StaticFieldSchema, - }).optional().describe("Create a new static field in a CMS collection (e.g., text, number, date, etc.)."), - // POST https://api.webflow.com/v2/collections/:collection_id/fields - create_collection_option_field:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - request: OptionFieldSchema, - }).optional().describe("Create a new option field in a CMS collection with predefined choices."), - // POST https://api.webflow.com/v2/collections/:collection_id/fields - create_collection_reference_field:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - request: ReferenceFieldSchema, - }).optional().describe("Create a new reference field in a CMS collection that links to items in another collection."), - // PATCH https://api.webflow.com/v2/collections/:collection_id/fields/:field_id - update_collection_field:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - field_id: z.string().describe("Unique identifier for the Field."), - request: WebflowCollectionsFieldUpdateSchema, - }).optional().describe("Update properties of an existing field in a CMS collection."), - // // POST https://api.webflow.com/v2/collections/:collection_id/items/live - // //NOTE: Cursor agent seems to struggle when provided with z.union(...), so we simplify the type here - // create_collection_items_live:z.object({ - // collection_id: z.string().describe("Unique identifier for the Collection."), - // request: WebflowCollectionsItemsCreateItemLiveRequestSchema, - // }).optional().describe("Create and publish new items in a CMS collection directly to the live site."), - // // PATCH https://api.webflow.com/v2/collections/:collection_id/items/live - // update_collection_items_live:z.object({ - // collection_id: z.string().describe("Unique identifier for the Collection."), - // request: WebflowCollectionsItemsUpdateItemsLiveRequestSchema, - // }).optional().describe("Update and publish existing items in a CMS collection directly to the live site."), - // GET https://api.webflow.com/v2/collections/:collection_id/items - list_collection_items:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - request:z.object({ - cmsLocaleId: z - .string() - .optional() - .describe("Unique identifier for the locale of the CMS Item."), - limit: z - .number() - .optional() - .describe("Maximum number of records to be returned (max limit: 100)"), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - name: z.string().optional().describe("Name of the field."), - slug: z - .string() - .optional() - .describe( - "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug." - ), - sortBy: WebflowCollectionsItemsListItemsRequestSortBySchema, - sortOrder: WebflowCollectionsItemsListItemsRequestSortOrderSchema, - }).optional().describe("Filter and sort items in a CMS collection."), - }).optional().describe("List items in a CMS collection with optional filtering and sorting."), - // POST https://api.webflow.com/v2/collections/:collection_id/items/bulk - create_collection_items:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - request: z.object({ - cmsLocaleIds:z.array(z.string()).optional().describe("Unique identifier for the locale of the CMS Item."), - isArchived:z.boolean().optional().describe("Indicates if the item is archived."), - isDraft:z.boolean().optional().describe("Indicates if the item is a draft."), - fieldData:z.array(z.record(z.any()).and(z.object({ - name:z.string().describe("Name of the field."), - slug:z.string().describe("URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug."), - }))).describe("Data of the item."), - }).describe("Array of items to be created."), - }).optional().describe("Create new items in a CMS collection as drafts."), - //PATCH https://api.webflow.com/v2/collections/:collection_id/items - update_collection_items:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - request: WebflowCollectionsItemsUpdateItemsRequestSchema.describe("Array of items to be updated."), - }).optional().describe("Update existing items in a CMS collection as drafts."), - // POST https://api.webflow.com/v2/collections/:collection_id/items/publish - publish_collection_items:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - request: z.object({ - itemIds: z.array(z.string()).describe("Array of item IDs to be published."), - }).describe("Array of items to be published."), - }).optional().describe("Publish existing items in a CMS collection as drafts."), - // DEL https://api.webflow.com/v2/collections/:collection_id/items - delete_collection_items:z.object({ - collection_id: z.string().describe("Unique identifier for the Collection."), - request: z.object({ - items: z.array(z.object({ - id: z.string().describe("Item ID to be deleted."), - cmsLocaleIds: z.array(z.string()).optional().describe("Unique identifier for the locale of the CMS Item."), - })).describe("Array of items to be deleted."), - }).describe("Array of items to be deleted."), - }).optional().describe("Delete existing items in a CMS collection as drafts."), - }) - ) - },async ({actions})=>{ - const result : Content[] = []; - try{ - for(const action of actions){ - if(action.get_collection_list){ - const content = await getCollectionList(action.get_collection_list); - result.push(textContent(content)); - }else if(action.get_collection_details){ - const content = await getCollectionDetails(action.get_collection_details); - result.push(textContent(content)); - }else if(action.create_collection){ - const content = await createCollection(action.create_collection); - result.push(textContent(content)); - }else if(action.create_collection_static_field){ - const content = await createCollectionStaticField(action.create_collection_static_field); - result.push(textContent(content)); - }else if(action.create_collection_option_field){ - const content = await createCollectionOptionField(action.create_collection_option_field); - result.push(textContent(content)); - }else if(action.create_collection_reference_field){ - const content = await createCollectionReferenceField(action.create_collection_reference_field); - result.push(textContent(content)); - } - else if(action.update_collection_field){ - const content = await updateCollectionField(action.update_collection_field); - result.push(textContent(content)); - } - // else if(action.create_collection_items_live){ - // const content = await createCollectionItemsLive(action.create_collection_items_live); - // result.push(textContent(content)); - // } - // else if(action.update_collection_items_live){ - // const content = await updateCollectionItemsLive(action.update_collection_items_live); - // result.push(textContent(content)); - // } - else if(action.list_collection_items){ - const content = await listCollectionItems({ - collection_id: action.list_collection_items.collection_id, - request: action.list_collection_items.request || {}, - }); - result.push(textContent(content)); - }else if(action.create_collection_items){ - const content = await createCollectionItems({ - collection_id: action.create_collection_items.collection_id, - request: action.create_collection_items.request, - }); - result.push(textContent(content)); - }else if(action.update_collection_items){ - const content = await updateCollectionItems({ - collection_id: action.update_collection_items.collection_id, - request: action.update_collection_items.request, - }); - result.push(textContent(content)); - }else if(action.publish_collection_items){ - const content = await publishCollectionItems({ - collection_id: action.publish_collection_items.collection_id, - request: action.publish_collection_items.request, - }); - result.push(textContent(content)); - }else if(action.delete_collection_items){ - const content = await deleteCollectionItems({ - collection_id: action.delete_collection_items.collection_id, - request: action.delete_collection_items.request, - }); - result.push(textContent(content)); + server.tool( + "data_cms_tool", + "Data tool - CMS tool to perform actions like get collection list, get collection details, create collection, create collection fields (static/option/reference), update collection field, list collection items, create collection items, update collection items, publish collection items, and delete collection items", + { + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites/:site_id/collections + get_collection_list: z + .object({ + ...SiteIdSchema, + }) + .optional() + .describe( + "List all CMS collections in a site. Returns collection metadata including IDs, names, and schemas." + ), + // GET https://api.webflow.com/v2/collections/:collection_id + get_collection_details: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + }) + .optional() + .describe( + "Get detailed information about a specific CMS collection including its schema and field definitions." + ), + // POST https://api.webflow.com/v2/sites/:site_id/collections + create_collection: z + .object({ + ...SiteIdSchema, + request: WebflowCollectionsCreateRequestSchema, + }) + .optional() + .describe( + "Create a new CMS collection in a site with specified name and schema." + ), + // POST https://api.webflow.com/v2/collections/:collection_id/fields + create_collection_static_field: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: StaticFieldSchema, + }) + .optional() + .describe( + "Create a new static field in a CMS collection (e.g., text, number, date, etc.)." + ), + // POST https://api.webflow.com/v2/collections/:collection_id/fields + create_collection_option_field: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: OptionFieldSchema, + }) + .optional() + .describe( + "Create a new option field in a CMS collection with predefined choices." + ), + // POST https://api.webflow.com/v2/collections/:collection_id/fields + create_collection_reference_field: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: ReferenceFieldSchema, + }) + .optional() + .describe( + "Create a new reference field in a CMS collection that links to items in another collection." + ), + // PATCH https://api.webflow.com/v2/collections/:collection_id/fields/:field_id + update_collection_field: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + field_id: z.string().describe("Unique identifier for the Field."), + request: WebflowCollectionsFieldUpdateSchema, + }) + .optional() + .describe( + "Update properties of an existing field in a CMS collection." + ), + // // POST https://api.webflow.com/v2/collections/:collection_id/items/live + // //NOTE: Cursor agent seems to struggle when provided with z.union(...), so we simplify the type here + // create_collection_items_live:z.object({ + // collection_id: z.string().describe("Unique identifier for the Collection."), + // request: WebflowCollectionsItemsCreateItemLiveRequestSchema, + // }).optional().describe("Create and publish new items in a CMS collection directly to the live site."), + // // PATCH https://api.webflow.com/v2/collections/:collection_id/items/live + // update_collection_items_live:z.object({ + // collection_id: z.string().describe("Unique identifier for the Collection."), + // request: WebflowCollectionsItemsUpdateItemsLiveRequestSchema, + // }).optional().describe("Update and publish existing items in a CMS collection directly to the live site."), + // GET https://api.webflow.com/v2/collections/:collection_id/items + list_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: z + .object({ + cmsLocaleId: z + .string() + .optional() + .describe( + "Unique identifier for the locale of the CMS Item." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + name: z.string().optional().describe("Name of the field."), + slug: z + .string() + .optional() + .describe( + "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug." + ), + sortBy: WebflowCollectionsItemsListItemsRequestSortBySchema, + sortOrder: + WebflowCollectionsItemsListItemsRequestSortOrderSchema, + }) + .optional() + .describe("Filter and sort items in a CMS collection."), + }) + .optional() + .describe( + "List items in a CMS collection with optional filtering and sorting." + ), + // POST https://api.webflow.com/v2/collections/:collection_id/items/bulk + create_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: z + .object({ + cmsLocaleIds: z + .array(z.string()) + .optional() + .describe( + "Unique identifier for the locale of the CMS Item." + ), + isArchived: z + .boolean() + .optional() + .describe("Indicates if the item is archived."), + isDraft: z + .boolean() + .optional() + .describe("Indicates if the item is a draft."), + fieldData: z + .array( + z.record(z.any()).and( + z.object({ + name: z.string().describe("Name of the field."), + slug: z + .string() + .describe( + "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug." + ), + }) + ) + ) + .describe("Data of the item."), + }) + .describe("Array of items to be created."), + }) + .optional() + .describe("Create new items in a CMS collection as drafts."), + //PATCH https://api.webflow.com/v2/collections/:collection_id/items + update_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: WebflowCollectionsItemsUpdateItemsRequestSchema.describe( + "Array of items to be updated." + ), + }) + .optional() + .describe("Update existing items in a CMS collection as drafts."), + // POST https://api.webflow.com/v2/collections/:collection_id/items/publish + publish_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: z + .object({ + itemIds: z + .array(z.string()) + .describe("Array of item IDs to be published."), + }) + .describe("Array of items to be published."), + }) + .optional() + .describe("Publish existing items in a CMS collection as drafts."), + // DEL https://api.webflow.com/v2/collections/:collection_id/items + delete_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: z + .object({ + items: z + .array( + z.object({ + id: z.string().describe("Item ID to be deleted."), + cmsLocaleIds: z + .array(z.string()) + .optional() + .describe( + "Unique identifier for the locale of the CMS Item." + ), + }) + ) + .describe("Array of items to be deleted."), + }) + .describe("Array of items to be deleted."), + }) + .optional() + .describe("Delete existing items in a CMS collection as drafts."), + }) + ), + }, + async ({ actions }) => { + const result: Content[] = []; + try { + for (const action of actions) { + if (action.get_collection_list) { + const content = await getCollectionList(action.get_collection_list); + result.push(textContent(content)); + } + if (action.get_collection_details) { + const content = await getCollectionDetails( + action.get_collection_details + ); + result.push(textContent(content)); + } + if (action.create_collection) { + const content = await createCollection(action.create_collection); + result.push(textContent(content)); + } + if (action.create_collection_static_field) { + const content = await createCollectionStaticField( + action.create_collection_static_field + ); + result.push(textContent(content)); + } + if (action.create_collection_option_field) { + const content = await createCollectionOptionField( + action.create_collection_option_field + ); + result.push(textContent(content)); + } + if (action.create_collection_reference_field) { + const content = await createCollectionReferenceField( + action.create_collection_reference_field + ); + result.push(textContent(content)); + } + if (action.update_collection_field) { + const content = await updateCollectionField( + action.update_collection_field + ); + result.push(textContent(content)); + } + // else if(action.create_collection_items_live){ + // const content = await createCollectionItemsLive(action.create_collection_items_live); + // result.push(textContent(content)); + // } + // else if(action.update_collection_items_live){ + // const content = await updateCollectionItemsLive(action.update_collection_items_live); + // result.push(textContent(content)); + // } + + if (action.list_collection_items) { + const content = await listCollectionItems({ + collection_id: action.list_collection_items.collection_id, + request: action.list_collection_items.request || {}, + }); + result.push(textContent(content)); + } + if (action.create_collection_items) { + const content = await createCollectionItems({ + collection_id: action.create_collection_items.collection_id, + request: action.create_collection_items.request, + }); + result.push(textContent(content)); + } + if (action.update_collection_items) { + const content = await updateCollectionItems({ + collection_id: action.update_collection_items.collection_id, + request: action.update_collection_items.request, + }); + result.push(textContent(content)); + } + if (action.publish_collection_items) { + const content = await publishCollectionItems({ + collection_id: action.publish_collection_items.collection_id, + request: action.publish_collection_items.request, + }); + result.push(textContent(content)); + } + if (action.delete_collection_items) { + const content = await deleteCollectionItems({ + collection_id: action.delete_collection_items.collection_id, + request: action.delete_collection_items.request, + }); + result.push(textContent(content)); + } + } + return toolResponse(result); + } catch (error) { + return formatErrorResponse(error); } } - return toolResponse(result); - } catch (error) { - return formatErrorResponse(error); - } - }); - - + ); } diff --git a/src/tools/components.ts b/src/tools/components.ts index ff5a15c..4e4c389 100644 --- a/src/tools/components.ts +++ b/src/tools/components.ts @@ -4,9 +4,14 @@ import { z } from "zod"; import { requestOptions } from "../mcp"; import { ComponentDomWriteNodesItemSchema, - ComponentPropertyUpdateSchema + ComponentPropertyUpdateSchema, } from "../schemas"; -import { Content, formatErrorResponse, textContent, toolResponse } from "../utils"; +import { + type Content, + formatErrorResponse, + textContent, + toolResponse, +} from "../utils"; export function registerComponentsTools( server: McpServer, @@ -237,22 +242,26 @@ export function registerComponentsTools( if (action.list_components) { const content = await listComponents(action.list_components); result.push(textContent(content)); - } else if (action.get_component_content) { + } + if (action.get_component_content) { const content = await getComponentContent( action.get_component_content ); result.push(textContent(content)); - } else if (action.update_component_content) { + } + if (action.update_component_content) { const content = await updateComponentContent( action.update_component_content ); result.push(textContent(content)); - } else if (action.get_component_properties) { + } + if (action.get_component_properties) { const content = await getComponentProperties( action.get_component_properties ); result.push(textContent(content)); - } else if (action.update_component_properties) { + } + if (action.update_component_properties) { const content = await updateComponentProperties( action.update_component_properties ); diff --git a/src/tools/pages.ts b/src/tools/pages.ts index e4613b6..0d26ca6 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -6,7 +6,12 @@ import { WebflowPageDomWriteNodesItemSchema, WebflowPageSchema, } from "../schemas"; -import { Content, formatErrorResponse, textContent, toolResponse } from "../utils"; +import { + type Content, + formatErrorResponse, + textContent, + toolResponse, +} from "../utils"; export function registerPagesTools( server: McpServer, @@ -212,17 +217,25 @@ export function registerPagesTools( if (action.list_pages) { const content = await listPages(action.list_pages); result.push(textContent(content)); - } else if (action.get_page_metadata) { + } + if (action.get_page_metadata) { const content = await getPageMetadata(action.get_page_metadata); result.push(textContent(content)); - } else if (action.update_page_settings) { - const content = await updatePageSettings(action.update_page_settings); + } + if (action.update_page_settings) { + const content = await updatePageSettings( + action.update_page_settings + ); result.push(textContent(content)); - } else if (action.get_page_content) { + } + if (action.get_page_content) { const content = await getPageContent(action.get_page_content); result.push(textContent(content)); - } else if (action.update_static_content) { - const content = await updateStaticContent(action.update_static_content); + } + if (action.update_static_content) { + const content = await updateStaticContent( + action.update_static_content + ); result.push(textContent(content)); } } diff --git a/src/tools/scripts.ts b/src/tools/scripts.ts index d888bc9..dcf6c2e 100644 --- a/src/tools/scripts.ts +++ b/src/tools/scripts.ts @@ -4,7 +4,13 @@ import { ScriptApplyLocation } from "webflow-api/api/types/ScriptApplyLocation"; import { z } from "zod"; import { requestOptions } from "../mcp"; import { RegisterInlineSiteScriptSchema } from "../schemas"; -import { Content, formatErrorResponse, textContent, toolResponse, isApiError } from "../utils"; +import { + type Content, + formatErrorResponse, + textContent, + toolResponse, + isApiError, +} from "../utils"; export function registerScriptsTools( server: McpServer, @@ -50,11 +56,10 @@ export function registerScriptsTools( let existingScripts: any[] = []; try { - const allScriptsResponse = - await getClient().sites.scripts.getCustomCode( - arg.site_id, - requestOptions - ); + const allScriptsResponse = await getClient().sites.scripts.getCustomCode( + arg.site_id, + requestOptions + ); existingScripts = allScriptsResponse.scripts || []; } catch (error) { existingScripts = []; @@ -154,17 +159,20 @@ export function registerScriptsTools( action.list_registered_scripts ); result.push(textContent(content)); - } else if (action.list_applied_scripts) { + } + if (action.list_applied_scripts) { const content = await listAppliedScripts( action.list_applied_scripts ); result.push(textContent(content)); - } else if (action.add_inline_site_script) { + } + if (action.add_inline_site_script) { const content = await addInlineSiteScript( action.add_inline_site_script ); result.push(textContent(content)); - } else if (action.delete_all_site_scripts) { + } + if (action.delete_all_site_scripts) { const content = await deleteAllSiteScripts( action.delete_all_site_scripts ); diff --git a/src/tools/sites.ts b/src/tools/sites.ts index 73d4142..ca8840c 100644 --- a/src/tools/sites.ts +++ b/src/tools/sites.ts @@ -2,7 +2,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WebflowClient } from "webflow-api"; import { z } from "zod"; import { requestOptions } from "../mcp"; -import { Content, formatErrorResponse, textContent, toolResponse } from "../utils"; +import { + type Content, + formatErrorResponse, + textContent, + toolResponse, +} from "../utils"; export function registerSiteTools( server: McpServer, @@ -83,10 +88,12 @@ export function registerSiteTools( if (action.list_sites) { const content = await listSites(); result.push(textContent(content)); - } else if (action.get_site) { + } + if (action.get_site) { const content = await getSite(action.get_site); result.push(textContent(content)); - } else if (action.publish_site) { + } + if (action.publish_site) { const content = await publishSite(action.publish_site); result.push(textContent(content)); } From 69cf0416cf430a643b8d64c5765353776e7cbc31 Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Thu, 13 Nov 2025 05:29:25 +0530 Subject: [PATCH 09/14] chore: update package dependencies and refactor tools for improved consistency - Updated dependencies in package.json and package-lock.json, including @modelcontextprotocol/sdk to version 1.21.1 and webflow-api to version 3.2.1. - Refactored various tools to use registerTool method for better consistency across the codebase. - Enhanced input schemas and descriptions for several tools, including data scripts, pages, and styles, to improve clarity and maintainability. - Removed deprecated or unnecessary code segments to streamline functionality. --- package-lock.json | 193 ++++++------ package.json | 13 +- src/mcp.ts | 10 +- src/schemas/StaticFieldSchema.ts | 2 +- src/tools/aiChat.ts | 14 +- src/tools/cms.ts | 472 +++++++++++++++--------------- src/tools/components.ts | 253 ++++++++-------- src/tools/deAsset.ts | 182 +++++------- src/tools/deComponents.ts | 169 +++++------ src/tools/deElement.ts | 439 ++++++++++++--------------- src/tools/dePages.ts | 144 ++++----- src/tools/deStyle.ts | 320 ++++++++++---------- src/tools/deVariable.ts | 412 +++++++++++++------------- src/tools/localDeMCPConnection.ts | 22 +- src/tools/pages.ts | 223 +++++++------- src/tools/rules.ts | 14 +- src/tools/scripts.ts | 93 +++--- src/tools/sites.ts | 81 ++--- 18 files changed, 1468 insertions(+), 1588 deletions(-) diff --git a/package-lock.json b/package-lock.json index feaa700..5f10b28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,11 @@ "name": "webflow-mcp-server", "version": "1.0.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.8.0", - "agents": "^0.0.59", + "@modelcontextprotocol/sdk": "1.21.1", "cors": "^2.8.5", "express": "^5.1.0", "socket.io": "^4.8.1", - "webflow-api": "3.1.1", + "webflow-api": "3.2.1", "zod": "^3.24.2" }, "bin": { @@ -29,13 +28,6 @@ "typescript": "^5.8.2" } }, - "node_modules/@cloudflare/workers-types": { - "version": "4.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250409.0.tgz", - "integrity": "sha512-yPxxwE5nr168huEfLNOB6904OsvIWcq0tWT23NMD6jT5SIp2ds3oOGANw7wz39r5y3jZYC2h1OnGwnZXJDDCOg==", - "license": "MIT OR Apache-2.0", - "peer": true - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -618,15 +610,18 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz", - "integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==", + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.1.tgz", + "integrity": "sha512-UyLFcJLDvUuZbGnaQqXFT32CpPpGj7VS19roLut6gkQVhb439xUzYWbsUvdI3ZPL+2hnFosuugtYWE0Mcs1rmQ==", "license": "MIT", "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", @@ -636,6 +631,14 @@ }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + } } }, "node_modules/@pkgjs/parseargs": { @@ -1082,17 +1085,37 @@ "node": ">= 0.6" } }, - "node_modules/agents": { - "version": "0.0.59", - "resolved": "https://registry.npmjs.org/agents/-/agents-0.0.59.tgz", - "integrity": "sha512-OiUg1QffKszZ+FdSB60KvBQF2Owzp96WIeP3XK2ShB3dYeAGwlsg6SmE7QomNfR+oe1KgvI4AmBKy9cLB7qY6A==", + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.9.0", - "cron-schedule": "^5.0.4", - "nanoid": "^5.1.5", - "partyserver": "^0.0.66", - "partysocket": "1.1.3" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, "node_modules/ansi-regex": { @@ -1764,15 +1787,6 @@ "sha.js": "^2.4.8" } }, - "node_modules/cron-schedule": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cron-schedule/-/cron-schedule-5.0.4.tgz", - "integrity": "sha512-nH0a49E/kSVk6BeFgKZy4uUsy6D2A16p120h5bYD9ILBhQu7o2sJFH+WI4R731TSBQ0dB1Ik7inB/dRAB4C8QQ==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2122,12 +2136,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-polyfill": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz", - "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==", - "license": "MIT" - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -2257,6 +2265,28 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2802,11 +2832,17 @@ } }, "node_modules/js-base64": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", - "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", "license": "BSD-3-Clause" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2992,24 +3028,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nanoid": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", - "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -3176,27 +3194,6 @@ "node": ">= 0.8" } }, - "node_modules/partyserver": { - "version": "0.0.66", - "resolved": "https://registry.npmjs.org/partyserver/-/partyserver-0.0.66.tgz", - "integrity": "sha512-GyC1uy4dvC4zPkwdzHqCkQ1J1CMiI0swIJQ0qqsJh16WNkEo5QHuU3l3ikLO8t+Yq0cRr0qO8++xbr11h+107w==", - "license": "ISC", - "dependencies": { - "nanoid": "^5.1.2" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20240729.0" - } - }, - "node_modules/partysocket": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.1.3.tgz", - "integrity": "sha512-87Jd/nqPoWnVfzHE6Z12WLWTJ+TAgxs0b7i2S163HfQSrVDUK5tW/FC64T5N8L5ss+gqF+EV0BwjZMWggMY3UA==", - "license": "ISC", - "dependencies": { - "event-target-polyfill": "^0.0.4" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3493,6 +3490,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -4426,35 +4432,20 @@ } }, "node_modules/webflow-api": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/webflow-api/-/webflow-api-3.1.1.tgz", - "integrity": "sha512-WypXB9Vz9fXx5cVUBtO9O702//FWFoDSvmDkw0VHh36PZ2X/XcKSYwT/uYMap2E4k242Zv8a63MUn6JkF36Rig==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/webflow-api/-/webflow-api-3.2.1.tgz", + "integrity": "sha512-NvBH15JPvVIdKHEDpzKzrJWnJytwqaBuUDVxlhW91fgK5hSr9kXrD3t67ZqIS1zzi3AnnGSVJl/sauJS0TqBDg==", "dependencies": { "crypto-browserify": "^3.12.1", "form-data": "^4.0.0", "formdata-node": "^6.0.3", - "js-base64": "3.7.2", - "node-fetch": "2.7.0", - "qs": "6.11.2", + "js-base64": "3.7.7", + "node-fetch": "^2.7.0", + "qs": "^6.13.1", "readable-stream": "^4.5.2", "url-join": "4.0.1" } }, - "node_modules/webflow-api/node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index c1d247f..55d155f 100644 --- a/package.json +++ b/package.json @@ -9,20 +9,15 @@ "start": "concurrently \"npm run dev:local\" \"npm run inspector:local\"", "dev:local": "npm run build:watch", "inspector:local": "npx @modelcontextprotocol/inspector -- nodemon --env-file=.env -q --watch dist dist/index.js", - "dev:cf": "wrangler dev src/index.worker.ts", - "inspector:cf": "npx @modelcontextprotocol/inspector", - "deploy:cf": "npm run build && wrangler deploy", - "types:cf": "wrangler types", - "build": "tsup src/index.ts src/index.worker.ts --external=cloudflare:workers --dts --clean", - "build:watch": "tsup src/index.ts src/index.worker.ts --external=cloudflare:workers --dts --watch" + "build": "tsup src/index.ts --dts --clean", + "build:watch": "tsup src/index.ts --dts --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.8.0", - "agents": "^0.0.59", + "@modelcontextprotocol/sdk": "1.21.1", "cors": "^2.8.5", "express": "^5.1.0", "socket.io": "^4.8.1", - "webflow-api": "3.1.1", + "webflow-api": "3.2.1", "zod": "^3.24.2" }, "devDependencies": { diff --git a/src/mcp.ts b/src/mcp.ts index ffb2161..701eaaf 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -53,10 +53,7 @@ export function registerTools( registerSiteTools(server, getClient); } -export function registerDesignerTools( - server: McpServer, - rpc: RPCType -) { +export function registerDesignerTools(server: McpServer, rpc: RPCType) { registerDEAssetTools(server, rpc); registerDEComponentsTools(server, rpc); registerDEElementTools(server, rpc); @@ -72,9 +69,6 @@ export function registerMiscTools(server: McpServer) { /** * IMPORTANT: registerLocalTools is only valid for OSS MCP Version */ -export function registerLocalTools( - server: McpServer, - rpc: RPCType -) { +export function registerLocalTools(server: McpServer, rpc: RPCType) { registerLocalDeMCPConnectionTools(server, rpc); } diff --git a/src/schemas/StaticFieldSchema.ts b/src/schemas/StaticFieldSchema.ts index 3d407f2..8df013f 100644 --- a/src/schemas/StaticFieldSchema.ts +++ b/src/schemas/StaticFieldSchema.ts @@ -24,7 +24,7 @@ export const StaticFieldSchema = z.object({ z.literal("PlainText"), z.literal("RichText"), z.literal("Switch"), - z.literal("Video"), + z.literal("VideoLink"), ]) .describe("Type of the field. Choose of these appropriate field types."), displayName: z.string().describe("Name of the field."), diff --git a/src/tools/aiChat.ts b/src/tools/aiChat.ts index 5db5fdf..04d70b5 100644 --- a/src/tools/aiChat.ts +++ b/src/tools/aiChat.ts @@ -6,10 +6,18 @@ const BASE_URL = "https://developers.webflow.com/"; const X_FERN_HOST = "developers.webflow.com"; export function registerAiChatTools(server: McpServer) { - server.tool( + server.registerTool( "ask_webflow_ai", - "Ask Webflow AI about anything related to Webflow API.", - { message: z.string() }, + { + description: "Ask Webflow AI about anything related to Webflow API.", + title: "Ask Webflow AI", + annotations: { + openWorldHint: true, + }, + inputSchema: { + message: z.string().describe("The message to ask Webflow AI about."), + }, + }, async ({ message }) => { const result = await postChat(message); return { diff --git a/src/tools/cms.ts b/src/tools/cms.ts index 9e497b0..ac0672e 100644 --- a/src/tools/cms.ts +++ b/src/tools/cms.ts @@ -205,243 +205,255 @@ export function registerCmsTools( return response; }; - server.tool( + server.registerTool( "data_cms_tool", - "Data tool - CMS tool to perform actions like get collection list, get collection details, create collection, create collection fields (static/option/reference), update collection field, list collection items, create collection items, update collection items, publish collection items, and delete collection items", { - actions: z.array( - z.object({ - // GET https://api.webflow.com/v2/sites/:site_id/collections - get_collection_list: z - .object({ - ...SiteIdSchema, - }) - .optional() - .describe( - "List all CMS collections in a site. Returns collection metadata including IDs, names, and schemas." - ), - // GET https://api.webflow.com/v2/collections/:collection_id - get_collection_details: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - }) - .optional() - .describe( - "Get detailed information about a specific CMS collection including its schema and field definitions." - ), - // POST https://api.webflow.com/v2/sites/:site_id/collections - create_collection: z - .object({ - ...SiteIdSchema, - request: WebflowCollectionsCreateRequestSchema, - }) - .optional() - .describe( - "Create a new CMS collection in a site with specified name and schema." - ), - // POST https://api.webflow.com/v2/collections/:collection_id/fields - create_collection_static_field: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: StaticFieldSchema, - }) - .optional() - .describe( - "Create a new static field in a CMS collection (e.g., text, number, date, etc.)." - ), - // POST https://api.webflow.com/v2/collections/:collection_id/fields - create_collection_option_field: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: OptionFieldSchema, - }) - .optional() - .describe( - "Create a new option field in a CMS collection with predefined choices." - ), - // POST https://api.webflow.com/v2/collections/:collection_id/fields - create_collection_reference_field: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: ReferenceFieldSchema, - }) - .optional() - .describe( - "Create a new reference field in a CMS collection that links to items in another collection." - ), - // PATCH https://api.webflow.com/v2/collections/:collection_id/fields/:field_id - update_collection_field: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - field_id: z.string().describe("Unique identifier for the Field."), - request: WebflowCollectionsFieldUpdateSchema, - }) - .optional() - .describe( - "Update properties of an existing field in a CMS collection." - ), - // // POST https://api.webflow.com/v2/collections/:collection_id/items/live - // //NOTE: Cursor agent seems to struggle when provided with z.union(...), so we simplify the type here - // create_collection_items_live:z.object({ - // collection_id: z.string().describe("Unique identifier for the Collection."), - // request: WebflowCollectionsItemsCreateItemLiveRequestSchema, - // }).optional().describe("Create and publish new items in a CMS collection directly to the live site."), - // // PATCH https://api.webflow.com/v2/collections/:collection_id/items/live - // update_collection_items_live:z.object({ - // collection_id: z.string().describe("Unique identifier for the Collection."), - // request: WebflowCollectionsItemsUpdateItemsLiveRequestSchema, - // }).optional().describe("Update and publish existing items in a CMS collection directly to the live site."), - // GET https://api.webflow.com/v2/collections/:collection_id/items - list_collection_items: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: z - .object({ - cmsLocaleId: z - .string() - .optional() - .describe( - "Unique identifier for the locale of the CMS Item." - ), - limit: z - .number() - .optional() - .describe( - "Maximum number of records to be returned (max limit: 100)" - ), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - name: z.string().optional().describe("Name of the field."), - slug: z - .string() - .optional() - .describe( - "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug." - ), - sortBy: WebflowCollectionsItemsListItemsRequestSortBySchema, - sortOrder: - WebflowCollectionsItemsListItemsRequestSortOrderSchema, - }) - .optional() - .describe("Filter and sort items in a CMS collection."), - }) - .optional() - .describe( - "List items in a CMS collection with optional filtering and sorting." - ), - // POST https://api.webflow.com/v2/collections/:collection_id/items/bulk - create_collection_items: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: z - .object({ - cmsLocaleIds: z - .array(z.string()) - .optional() - .describe( - "Unique identifier for the locale of the CMS Item." - ), - isArchived: z - .boolean() - .optional() - .describe("Indicates if the item is archived."), - isDraft: z - .boolean() - .optional() - .describe("Indicates if the item is a draft."), - fieldData: z - .array( - z.record(z.any()).and( + title: "Data CMS Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Data tool - CMS tool to perform actions like get collection list, get collection details, create collection, create collection fields (static/option/reference), update collection field, list collection items, create collection items, update collection items, publish collection items, and delete collection items", + inputSchema: { + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites/:site_id/collections + get_collection_list: z + .object({ + ...SiteIdSchema, + }) + .optional() + .describe( + "List all CMS collections in a site. Returns collection metadata including IDs, names, and schemas." + ), + // GET https://api.webflow.com/v2/collections/:collection_id + get_collection_details: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + }) + .optional() + .describe( + "Get detailed information about a specific CMS collection including its schema and field definitions." + ), + // POST https://api.webflow.com/v2/sites/:site_id/collections + create_collection: z + .object({ + ...SiteIdSchema, + request: WebflowCollectionsCreateRequestSchema, + }) + .optional() + .describe( + "Create a new CMS collection in a site with specified name and schema." + ), + // POST https://api.webflow.com/v2/collections/:collection_id/fields + create_collection_static_field: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: StaticFieldSchema, + }) + .optional() + .describe( + "Create a new static field in a CMS collection (e.g., text, number, date, etc.)." + ), + // POST https://api.webflow.com/v2/collections/:collection_id/fields + create_collection_option_field: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: OptionFieldSchema, + }) + .optional() + .describe( + "Create a new option field in a CMS collection with predefined choices." + ), + // POST https://api.webflow.com/v2/collections/:collection_id/fields + create_collection_reference_field: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: ReferenceFieldSchema, + }) + .optional() + .describe( + "Create a new reference field in a CMS collection that links to items in another collection." + ), + // PATCH https://api.webflow.com/v2/collections/:collection_id/fields/:field_id + update_collection_field: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + field_id: z + .string() + .describe("Unique identifier for the Field."), + request: WebflowCollectionsFieldUpdateSchema, + }) + .optional() + .describe( + "Update properties of an existing field in a CMS collection." + ), + // // POST https://api.webflow.com/v2/collections/:collection_id/items/live + // //NOTE: Cursor agent seems to struggle when provided with z.union(...), so we simplify the type here + // create_collection_items_live:z.object({ + // collection_id: z.string().describe("Unique identifier for the Collection."), + // request: WebflowCollectionsItemsCreateItemLiveRequestSchema, + // }).optional().describe("Create and publish new items in a CMS collection directly to the live site."), + // // PATCH https://api.webflow.com/v2/collections/:collection_id/items/live + // update_collection_items_live:z.object({ + // collection_id: z.string().describe("Unique identifier for the Collection."), + // request: WebflowCollectionsItemsUpdateItemsLiveRequestSchema, + // }).optional().describe("Update and publish existing items in a CMS collection directly to the live site."), + // GET https://api.webflow.com/v2/collections/:collection_id/items + list_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: z + .object({ + cmsLocaleId: z + .string() + .optional() + .describe( + "Unique identifier for the locale of the CMS Item." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + name: z.string().optional().describe("Name of the field."), + slug: z + .string() + .optional() + .describe( + "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug." + ), + sortBy: WebflowCollectionsItemsListItemsRequestSortBySchema, + sortOrder: + WebflowCollectionsItemsListItemsRequestSortOrderSchema, + }) + .optional() + .describe("Filter and sort items in a CMS collection."), + }) + .optional() + .describe( + "List items in a CMS collection with optional filtering and sorting." + ), + // POST https://api.webflow.com/v2/collections/:collection_id/items/bulk + create_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: z + .object({ + cmsLocaleIds: z + .array(z.string()) + .optional() + .describe( + "Unique identifier for the locale of the CMS Item." + ), + isArchived: z + .boolean() + .optional() + .describe("Indicates if the item is archived."), + isDraft: z + .boolean() + .optional() + .describe("Indicates if the item is a draft."), + fieldData: z + .array( + z.record(z.any()).and( + z.object({ + name: z.string().describe("Name of the field."), + slug: z + .string() + .describe( + "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug." + ), + }) + ) + ) + .describe("Data of the item."), + }) + .describe("Array of items to be created."), + }) + .optional() + .describe("Create new items in a CMS collection as drafts."), + //PATCH https://api.webflow.com/v2/collections/:collection_id/items + update_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: + WebflowCollectionsItemsUpdateItemsRequestSchema.describe( + "Array of items to be updated." + ), + }) + .optional() + .describe("Update existing items in a CMS collection as drafts."), + // POST https://api.webflow.com/v2/collections/:collection_id/items/publish + publish_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: z + .object({ + itemIds: z + .array(z.string()) + .describe("Array of item IDs to be published."), + }) + .describe("Array of items to be published."), + }) + .optional() + .describe( + "Publish existing items in a CMS collection as drafts." + ), + // DEL https://api.webflow.com/v2/collections/:collection_id/items + delete_collection_items: z + .object({ + collection_id: z + .string() + .describe("Unique identifier for the Collection."), + request: z + .object({ + items: z + .array( z.object({ - name: z.string().describe("Name of the field."), - slug: z - .string() + id: z.string().describe("Item ID to be deleted."), + cmsLocaleIds: z + .array(z.string()) + .optional() .describe( - "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug." + "Unique identifier for the locale of the CMS Item." ), }) ) - ) - .describe("Data of the item."), - }) - .describe("Array of items to be created."), - }) - .optional() - .describe("Create new items in a CMS collection as drafts."), - //PATCH https://api.webflow.com/v2/collections/:collection_id/items - update_collection_items: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: WebflowCollectionsItemsUpdateItemsRequestSchema.describe( - "Array of items to be updated." - ), - }) - .optional() - .describe("Update existing items in a CMS collection as drafts."), - // POST https://api.webflow.com/v2/collections/:collection_id/items/publish - publish_collection_items: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: z - .object({ - itemIds: z - .array(z.string()) - .describe("Array of item IDs to be published."), - }) - .describe("Array of items to be published."), - }) - .optional() - .describe("Publish existing items in a CMS collection as drafts."), - // DEL https://api.webflow.com/v2/collections/:collection_id/items - delete_collection_items: z - .object({ - collection_id: z - .string() - .describe("Unique identifier for the Collection."), - request: z - .object({ - items: z - .array( - z.object({ - id: z.string().describe("Item ID to be deleted."), - cmsLocaleIds: z - .array(z.string()) - .optional() - .describe( - "Unique identifier for the locale of the CMS Item." - ), - }) - ) - .describe("Array of items to be deleted."), - }) - .describe("Array of items to be deleted."), - }) - .optional() - .describe("Delete existing items in a CMS collection as drafts."), - }) - ), + .describe("Array of items to be deleted."), + }) + .describe("Array of items to be deleted."), + }) + .optional() + .describe("Delete existing items in a CMS collection as drafts."), + }) + ), + }, }, async ({ actions }) => { const result: Content[] = []; diff --git a/src/tools/components.ts b/src/tools/components.ts index 4e4c389..48f00e9 100644 --- a/src/tools/components.ts +++ b/src/tools/components.ts @@ -109,131 +109,138 @@ export function registerComponentsTools( return response; }; - server.tool( + server.registerTool( "data_components_tool", - "Data tool - Components tool to perform actions like list components, get component content, update component content, get component properties, and update component properties", { - actions: z.array( - z.object({ - // GET https://api.webflow.com/v2/sites/:site_id/components - list_components: z - .object({ - site_id: z.string().describe("Unique identifier for the Site."), - limit: z - .number() - .optional() - .describe( - "Maximum number of records to be returned (max limit: 100)" - ), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }) - .optional() - .describe( - "List all components in a site. Returns component metadata including IDs, names, and versions." - ), - // GET https://api.webflow.com/v2/sites/:site_id/components/:component_id/dom - get_component_content: z - .object({ - site_id: z.string().describe("Unique identifier for the Site."), - component_id: z - .string() - .describe("Unique identifier for the Component."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - limit: z - .number() - .optional() - .describe( - "Maximum number of records to be returned (max limit: 100)" - ), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }) - .optional() - .describe( - "Get the content structure and data for a specific component including text, images, and nested components." - ), - // POST https://api.webflow.com/v2/sites/:site_id/components/:component_id/dom - update_component_content: z - .object({ - site_id: z.string().describe("Unique identifier for the Site."), - component_id: z - .string() - .describe("Unique identifier for the Component."), - localeId: z - .string() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - nodes: ComponentDomWriteNodesItemSchema, - }) - .optional() - .describe( - "Update content on a component in secondary locales by modifying text nodes and property overrides." - ), - // GET https://api.webflow.com/v2/sites/:site_id/components/:component_id/properties - get_component_properties: z - .object({ - site_id: z.string().describe("Unique identifier for the Site."), - component_id: z - .string() - .describe("Unique identifier for the Component."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - limit: z - .number() - .optional() - .describe( - "Maximum number of records to be returned (max limit: 100)" - ), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }) - .optional() - .describe( - "Get component properties including default values and configuration for a specific component." - ), - // POST https://api.webflow.com/v2/sites/:site_id/components/:component_id/properties - update_component_properties: z - .object({ - site_id: z.string().describe("Unique identifier for the Site."), - component_id: z - .string() - .describe("Unique identifier for the Component."), - localeId: z - .string() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - properties: ComponentPropertyUpdateSchema, - }) - .optional() - .describe( - "Update component properties for localization to customize behavior in different languages." - ), - }) - ), + title: "Data Components Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Data tool - Components tool to perform actions like list components, get component content, update component content, get component properties, and update component properties", + inputSchema: { + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites/:site_id/components + list_components: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "List all components in a site. Returns component metadata including IDs, names, and versions." + ), + // GET https://api.webflow.com/v2/sites/:site_id/components/:component_id/dom + get_component_content: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + component_id: z + .string() + .describe("Unique identifier for the Component."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "Get the content structure and data for a specific component including text, images, and nested components." + ), + // POST https://api.webflow.com/v2/sites/:site_id/components/:component_id/dom + update_component_content: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + component_id: z + .string() + .describe("Unique identifier for the Component."), + localeId: z + .string() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + nodes: ComponentDomWriteNodesItemSchema, + }) + .optional() + .describe( + "Update content on a component in secondary locales by modifying text nodes and property overrides." + ), + // GET https://api.webflow.com/v2/sites/:site_id/components/:component_id/properties + get_component_properties: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + component_id: z + .string() + .describe("Unique identifier for the Component."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "Get component properties including default values and configuration for a specific component." + ), + // POST https://api.webflow.com/v2/sites/:site_id/components/:component_id/properties + update_component_properties: z + .object({ + site_id: z.string().describe("Unique identifier for the Site."), + component_id: z + .string() + .describe("Unique identifier for the Component."), + localeId: z + .string() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + properties: ComponentPropertyUpdateSchema, + }) + .optional() + .describe( + "Update component properties for localization to customize behavior in different languages." + ), + }) + ), + }, }, async ({ actions }) => { const result: Content[] = []; diff --git a/src/tools/deAsset.ts b/src/tools/deAsset.ts index 0257f02..97c498e 100644 --- a/src/tools/deAsset.ts +++ b/src/tools/deAsset.ts @@ -2,142 +2,120 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { RPCType } from "../types/RPCType"; import z from "zod"; import { SiteIdSchema } from "../schemas"; -import { - formatErrorResponse, - formatResponse, -} from "../utils"; +import { formatErrorResponse, formatResponse } from "../utils"; -export function registerDEAssetTools( - server: McpServer, - rpc: RPCType -) { - const assetToolRPCCall = async ( - siteId: string, - actions: any - ) => { +export function registerDEAssetTools(server: McpServer, rpc: RPCType) { + const assetToolRPCCall = async (siteId: string, actions: any) => { return rpc.callTool("asset_tool", { siteId, actions: actions || [], }); }; - const getImagePreviewFromURL = async ( - url: string, - siteId: string - ) => { + const getImagePreviewFromURL = async (url: string, siteId: string) => { const response = await fetch(url); - const contentType = - response.headers.get("content-type"); + const contentType = response.headers.get("content-type"); if (!contentType || !contentType.startsWith("image/")) { throw new Error( - `Expected an image but received MIME type: ${ - contentType || "unknown" - }` + `Expected an image but received MIME type: ${contentType || "unknown"}` ); } const arrayBuffer = await response.arrayBuffer(); - const binary = String.fromCharCode( - ...new Uint8Array(arrayBuffer) - ); + const binary = String.fromCharCode(...new Uint8Array(arrayBuffer)); const base64 = btoa(binary); return { data: base64, mimeType: contentType, siteId }; }; - server.tool( + server.registerTool( "asset_tool", - "Designer Tool - Asset tool to perform actions like create folder, get all assets and folders, update assets and folders", { - ...SiteIdSchema, - actions: z.array( - z.object({ - create_folder: z - .object({ - name: z - .string() - .describe( - "The name of the folder to create" - ), - parent_folder_id: z - .string() - .optional() - .describe( - "The id of the parent folder to move the folder to." - ), - }) - .optional() - .describe("Create a folder on the site"), - get_all_assets_and_folders: z - .object({ - query: z - .enum(["all", "folders", "assets"]) - .describe( - "Query to get all assets and folders on the site" - ), - filter_assets_by_ids: z - .array(z.string()) - .describe("Filter assets by ids") - .optional(), - }) - .optional() - .describe( - "Get all assets and folders on the site" - ), - update_asset: z - .object({ - asset_id: z - .string() - .describe("The id of the asset to update"), - name: z - .string() - .optional() - .describe( - "The name of the asset to update" - ), - alt_text: z - .string() - .optional() - .describe( - "The alt text of the asset to update" - ), - parent_folder_id: z - .string() - .optional() - .describe( - "The id of the parent folder to move the asset to." - ), - }) - .optional() - .describe("Update an asset on the site"), - }) - ), + title: "Designer Asset Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Designer Tool - Asset tool to perform actions like create folder, get all assets and folders, update assets and folders", + inputSchema: { + ...SiteIdSchema, + actions: z.array( + z.object({ + create_folder: z + .object({ + name: z.string().describe("The name of the folder to create"), + parent_folder_id: z + .string() + .optional() + .describe( + "The id of the parent folder to move the folder to." + ), + }) + .optional() + .describe("Create a folder on the site"), + get_all_assets_and_folders: z + .object({ + query: z + .enum(["all", "folders", "assets"]) + .describe("Query to get all assets and folders on the site"), + filter_assets_by_ids: z + .array(z.string()) + .describe("Filter assets by ids") + .optional(), + }) + .optional() + .describe("Get all assets and folders on the site"), + update_asset: z + .object({ + asset_id: z.string().describe("The id of the asset to update"), + name: z + .string() + .optional() + .describe("The name of the asset to update"), + alt_text: z + .string() + .optional() + .describe("The alt text of the asset to update"), + parent_folder_id: z + .string() + .optional() + .describe( + "The id of the parent folder to move the asset to." + ), + }) + .optional() + .describe("Update an asset on the site"), + }) + ), + }, }, async ({ siteId, actions }) => { try { - return formatResponse( - await assetToolRPCCall(siteId, actions) - ); + return formatResponse(await assetToolRPCCall(siteId, actions)); } catch (error) { return formatErrorResponse(error); } } ); - server.tool( + server.registerTool( "get_image_preview", - "Designer Tool - Get image preview from url. this is helpful to get image preview from url.", { - url: z - .string() - .describe( - "The URL of the image to get the preview from" - ), - ...SiteIdSchema, + title: "Get Webflow Image Preview", + annotations: { + readOnlyHint: false, + }, + description: + "Designer Tool - Get image preview from url. this is helpful to get image preview from url. Only supports JPG, PNG, GIF, WEBP, WEBP and AVIF formats.", + inputSchema: { + url: z + .string() + .describe("The URL of the image to get the preview from"), + ...SiteIdSchema, + }, }, async ({ url, siteId }) => { try { - const { data, mimeType } = - await getImagePreviewFromURL(url, siteId); + const { data, mimeType } = await getImagePreviewFromURL(url, siteId); return { content: [ { diff --git a/src/tools/deComponents.ts b/src/tools/deComponents.ts index ba3235f..ea73d0a 100644 --- a/src/tools/deComponents.ts +++ b/src/tools/deComponents.ts @@ -1,112 +1,93 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { RPCType } from "../types/RPCType"; import z from "zod"; -import { - DEElementIDSchema, - SiteIdSchema, -} from "../schemas"; -import { - formatErrorResponse, - formatResponse, -} from "../utils"; +import { DEElementIDSchema, SiteIdSchema } from "../schemas"; +import { formatErrorResponse, formatResponse } from "../utils"; -export function registerDEComponentsTools( - server: McpServer, - rpc: RPCType -) { - const componentsToolRPCCall = async ( - siteId: string, - actions: any - ) => { +export function registerDEComponentsTools(server: McpServer, rpc: RPCType) { + const componentsToolRPCCall = async (siteId: string, actions: any) => { return rpc.callTool("component_tool", { siteId, actions: actions || [], }); }; - server.tool( + server.registerTool( "de_component_tool", - "Designer tool - Component tool to perform actions like create component instances, get all components and more.", { - ...SiteIdSchema, - actions: z.array( - z.object({ - check_if_inside_component_view: z - .boolean() - .optional() - .describe( - "Check if inside component view. this helpful to make changes to the component" - ), - transform_element_to_component: z - .object({ - ...DEElementIDSchema, - name: z - .string() - .describe("The name of the component"), - }) - .optional() - .describe( - "Transform an element to a component" - ), - insert_component_instance: z - .object({ - parent_element_id: DEElementIDSchema.id, - component_id: z - .string() - .describe( - "The id of the component to insert" - ), - creation_position: z - .enum(["append", "prepend"]) - .describe( - "The position to create component instance on. append to the end of the parent element or prepend to the beginning of the parent element. as child of the parent element." - ), - }) - .optional() - .describe( - "Insert a component on current active page." - ), - open_component_view: z - .object({ - component_instance_id: DEElementIDSchema.id, - }) - .optional() - .describe( - "Open a component instance view for changes or reading." - ), - close_component_view: z - .boolean() - .optional() - .describe( - "Close a component instance view. it will close and open the page view." - ), - get_all_components: z - .boolean() - .optional() - .describe( - "Get all components, only valid if you are connected to Webflow Designer." - ), - rename_component: z - .object({ - component_id: z - .string() - .describe( - "The id of the component to rename" - ), - new_name: z - .string() - .describe("The name of the component"), - }) - .optional() - .describe("Rename a component."), - }) - ), + title: "Designer Component Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Designer tool - Component tool to perform actions like create component instances, get all components and more.", + inputSchema: { + ...SiteIdSchema, + actions: z.array( + z.object({ + check_if_inside_component_view: z + .boolean() + .optional() + .describe( + "Check if inside component view. this helpful to make changes to the component" + ), + transform_element_to_component: z + .object({ + ...DEElementIDSchema, + name: z.string().describe("The name of the component"), + }) + .optional() + .describe("Transform an element to a component"), + insert_component_instance: z + .object({ + parent_element_id: DEElementIDSchema.id, + component_id: z + .string() + .describe("The id of the component to insert"), + creation_position: z + .enum(["append", "prepend"]) + .describe( + "The position to create component instance on. append to the end of the parent element or prepend to the beginning of the parent element. as child of the parent element." + ), + }) + .optional() + .describe("Insert a component on current active page."), + open_component_view: z + .object({ + component_instance_id: DEElementIDSchema.id, + }) + .optional() + .describe( + "Open a component instance view for changes or reading." + ), + close_component_view: z + .boolean() + .optional() + .describe( + "Close a component instance view. it will close and open the page view." + ), + get_all_components: z + .boolean() + .optional() + .describe( + "Get all components, only valid if you are connected to Webflow Designer." + ), + rename_component: z + .object({ + component_id: z + .string() + .describe("The id of the component to rename"), + new_name: z.string().describe("The name of the component"), + }) + .optional() + .describe("Rename a component."), + }) + ), + }, }, async ({ siteId, actions }) => { try { - return formatResponse( - await componentsToolRPCCall(siteId, actions) - ); + return formatResponse(await componentsToolRPCCall(siteId, actions)); } catch (error) { return formatErrorResponse(error); } diff --git a/src/tools/deElement.ts b/src/tools/deElement.ts index 47e13c3..4605a68 100644 --- a/src/tools/deElement.ts +++ b/src/tools/deElement.ts @@ -1,284 +1,229 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { RPCType } from "../types/RPCType"; import z from "zod"; -import { - SiteIdSchema, - DEElementIDSchema, - DEElementSchema, -} from "../schemas"; -import { - formatErrorResponse, - formatResponse, -} from "../utils"; +import { SiteIdSchema, DEElementIDSchema, DEElementSchema } from "../schemas"; +import { formatErrorResponse, formatResponse } from "../utils"; -export const registerDEElementTools = ( - server: McpServer, - rpc: RPCType -) => { - const elementBuilderRPCCall = async ( - siteId: string, - actions: any - ) => { +export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { + const elementBuilderRPCCall = async (siteId: string, actions: any) => { return rpc.callTool("element_builder", { siteId, actions: actions || [], }); }; - const elementToolRPCCall = async ( - siteId: string, - actions: any - ) => { + const elementToolRPCCall = async (siteId: string, actions: any) => { return rpc.callTool("element_tool", { siteId, actions: actions || [], }); }; - server.tool( + server.registerTool( "element_builder", - "Designer Tool - Element builder to create element on current active page. only create elements upto max 3 levels deep. divide your elements into smaller elements to create complex structures. recall this tool to create more elements. but max level is upto 3 levels. you can have as many children as you want. but max level is 3 levels.", { - ...SiteIdSchema, - actions: z.array( - z.object({ - parent_element_id: z - .object({ - component: z - .string() - .describe( - "The component id of the element to perform action on." - ), - element: z - .string() - .describe( - "The element id of the element to perform action on." - ), - }) - .describe( - "The id of the parent element to create element on, you can find it from id field on element. e.g id:{component:123,element:456}." - ), - creation_position: z - .enum(["append", "prepend"]) - .describe( - "The position to create element on. append to the end of the parent element or prepend to the beginning of the parent element. as child of the parent element." - ), - element_schema: DEElementSchema.extend({ - children: z - .array( - DEElementSchema.extend({ - children: z - .array( - DEElementSchema.extend({ - children: z - .array( - DEElementSchema.extend({ - children: z - .array(DEElementSchema) - .optional(), - }) - ) - .optional(), - }) - ) - .optional(), - }) - ) - .optional() + description: + "Designer Tool - Element builder to create element on current active page. only create elements upto max 3 levels deep. divide your elements into smaller elements to create complex structures. recall this tool to create more elements. but max level is upto 3 levels. you can have as many children as you want. but max level is 3 levels.", + inputSchema: { + ...SiteIdSchema, + actions: z.array( + z.object({ + parent_element_id: z + .object({ + component: z + .string() + .describe( + "The component id of the element to perform action on." + ), + element: z + .string() + .describe( + "The element id of the element to perform action on." + ), + }) + .describe( + "The id of the parent element to create element on, you can find it from id field on element. e.g id:{component:123,element:456}." + ), + creation_position: z + .enum(["append", "prepend"]) .describe( - "The children of the element. only valid for container, section, div block, valid DOM elements." + "The position to create element on. append to the end of the parent element or prepend to the beginning of the parent element. as child of the parent element." ), - }).describe( - "element schema of element to create." - ), - }) - ), + element_schema: DEElementSchema.extend({ + children: z + .array( + DEElementSchema.extend({ + children: z + .array( + DEElementSchema.extend({ + children: z + .array( + DEElementSchema.extend({ + children: z.array(DEElementSchema).optional(), + }) + ) + .optional(), + }) + ) + .optional(), + }) + ) + .optional() + .describe( + "The children of the element. only valid for container, section, div block, valid DOM elements." + ), + }).describe("element schema of element to create."), + }) + ), + }, }, async ({ actions, siteId }) => { try { - return formatResponse( - await elementBuilderRPCCall(siteId, actions) - ); + return formatResponse(await elementBuilderRPCCall(siteId, actions)); } catch (error) { return formatErrorResponse(error); } } ); - server.tool( + server.registerTool( "element_tool", - "Designer Tool - Element tool to perform actions like get all elements, get selected element, select element on current active page. and more", { - ...SiteIdSchema, - actions: z.array( - z.object({ - get_all_elements: z - .object({ - query: z - .enum(["all"]) - .describe("Query to get all elements"), - include_style_properties: z - .boolean() - .optional() - .describe("Include style properties"), - include_all_breakpoint_styles: z - .boolean() - .optional() - .describe("Include all breakpoints styles"), - }) - .optional() - .describe( - "Get all elements on the current active page" - ), - get_selected_element: z - .boolean() - .optional() - .describe( - "Get selected element on the current active page" - ), - select_element: z - .object({ - ...DEElementIDSchema, - }) - .optional() - .describe( - "Select an element on the current active page" - ), - add_or_update_attribute: z - .object({ - ...DEElementIDSchema, - attributes: z - .array( - z.object({ - name: z - .string() - .describe( - "The name of the attribute to add or update." - ), - value: z - .string() - .describe( - "The value of the attribute to add or update." - ), - }) - ) - .describe( - "The attributes to add or update." - ), - }) - .optional() - .describe( - "Add or update an attribute on the element" - ), - remove_attribute: z - .object({ - ...DEElementIDSchema, - attribute_names: z - .array(z.string()) - .describe( - "The names of the attributes to remove." - ), - }) - .optional() - .describe( - "Remove an attribute from the element" - ), - update_id_attribute: z - .object({ - ...DEElementIDSchema, - new_id: z - .string() - .describe( - "The new #id of the element to update the id attribute to." - ), - }) - .optional() - .describe( - "Update the #id attribute of the element" - ), - set_text: z - .object({ - ...DEElementIDSchema, - text: z - .string() - .describe( - "The text to set on the element." - ), - }) - .optional() - .describe("Set text on the element"), - set_style: z - .object({ - ...DEElementIDSchema, - style_names: z - .array(z.string()) - .describe( - "The style names to set on the element." - ), - }) - .optional() - .describe( - "Set style on the element. it will remove all other styles on the element. and set only the styles passed in style_names." - ), - set_link: z - .object({ - ...DEElementIDSchema, - linkType: z - .enum([ - "url", - "file", - "page", - "element", - "email", - "phone", - ]) - .describe( - "The type of the link to update." - ), - link: z - .string() - .describe( - "The link to set on the element. for page pass page id, for element pass json string of id object. e.g id:{component:123,element:456}. for email pass email address. for phone pass phone number. for file pass asset id. for url pass url." - ), - }) - .optional() - .describe("Set link on the element"), - set_heading_level: z - .object({ - ...DEElementIDSchema, - heading_level: z - .number() - .min(1) - .max(6) - .describe( - "The heading level to set on the element. 1 to 6." - ), - }) - .optional() - .describe( - "Set heading level on the heading element." - ), - set_image_asset: z - .object({ - ...DEElementIDSchema, - image_asset_id: z - .string() - .describe( - "The image asset id to set on the element." - ), - }) - .optional() - .describe( - "Set image asset on the image element" - ), - }) - ), + title: "Designer Element Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Designer Tool - Element tool to perform actions like get all elements, get selected element, select element on current active page. and more", + inputSchema: { + ...SiteIdSchema, + actions: z.array( + z.object({ + get_all_elements: z + .object({ + query: z.enum(["all"]).describe("Query to get all elements"), + include_style_properties: z + .boolean() + .optional() + .describe("Include style properties"), + include_all_breakpoint_styles: z + .boolean() + .optional() + .describe("Include all breakpoints styles"), + }) + .optional() + .describe("Get all elements on the current active page"), + get_selected_element: z + .boolean() + .optional() + .describe("Get selected element on the current active page"), + select_element: z + .object({ + ...DEElementIDSchema, + }) + .optional() + .describe("Select an element on the current active page"), + add_or_update_attribute: z + .object({ + ...DEElementIDSchema, + attributes: z + .array( + z.object({ + name: z + .string() + .describe( + "The name of the attribute to add or update." + ), + value: z + .string() + .describe( + "The value of the attribute to add or update." + ), + }) + ) + .describe("The attributes to add or update."), + }) + .optional() + .describe("Add or update an attribute on the element"), + remove_attribute: z + .object({ + ...DEElementIDSchema, + attribute_names: z + .array(z.string()) + .describe("The names of the attributes to remove."), + }) + .optional() + .describe("Remove an attribute from the element"), + update_id_attribute: z + .object({ + ...DEElementIDSchema, + new_id: z + .string() + .describe( + "The new #id of the element to update the id attribute to." + ), + }) + .optional() + .describe("Update the #id attribute of the element"), + set_text: z + .object({ + ...DEElementIDSchema, + text: z.string().describe("The text to set on the element."), + }) + .optional() + .describe("Set text on the element"), + set_style: z + .object({ + ...DEElementIDSchema, + style_names: z + .array(z.string()) + .describe("The style names to set on the element."), + }) + .optional() + .describe( + "Set style on the element. it will remove all other styles on the element. and set only the styles passed in style_names." + ), + set_link: z + .object({ + ...DEElementIDSchema, + linkType: z + .enum(["url", "file", "page", "element", "email", "phone"]) + .describe("The type of the link to update."), + link: z + .string() + .describe( + "The link to set on the element. for page pass page id, for element pass json string of id object. e.g id:{component:123,element:456}. for email pass email address. for phone pass phone number. for file pass asset id. for url pass url." + ), + }) + .optional() + .describe("Set link on the element"), + set_heading_level: z + .object({ + ...DEElementIDSchema, + heading_level: z + .number() + .min(1) + .max(6) + .describe("The heading level to set on the element. 1 to 6."), + }) + .optional() + .describe("Set heading level on the heading element."), + set_image_asset: z + .object({ + ...DEElementIDSchema, + image_asset_id: z + .string() + .describe("The image asset id to set on the element."), + }) + .optional() + .describe("Set image asset on the image element"), + }) + ), + }, }, async ({ actions, siteId }) => { try { - return formatResponse( - await elementToolRPCCall(siteId, actions) - ); + return formatResponse(await elementToolRPCCall(siteId, actions)); } catch (error) { return formatErrorResponse(error); } diff --git a/src/tools/dePages.ts b/src/tools/dePages.ts index 4cde9a4..58451aa 100644 --- a/src/tools/dePages.ts +++ b/src/tools/dePages.ts @@ -2,100 +2,82 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { RPCType } from "../types/RPCType"; import z from "zod"; import { SiteIdSchema } from "../schemas"; -import { - formatErrorResponse, - formatResponse, -} from "../utils"; +import { formatErrorResponse, formatResponse } from "../utils"; -export function registerDEPagesTools( - server: McpServer, - rpc: RPCType -) { - const pageToolRPCCall = async ( - siteId: string, - actions: any - ) => { +export function registerDEPagesTools(server: McpServer, rpc: RPCType) { + const pageToolRPCCall = async (siteId: string, actions: any) => { return rpc.callTool("page_tool", { siteId, actions: actions || [], }); }; - server.tool( + server.registerTool( "de_page_tool", - "Designer Tool - Page tool to perform actions like create page, create page folder, get current page, switch page", { - ...SiteIdSchema, - actions: z.array( - z.object({ - create_page: z - .object({ - page_name: z - .string() - .describe("The name of the page to create"), - meta_title: z - .string() - .describe( - "The meta title of the page to create" - ), - meta_description: z - .string() - .optional() - .describe( - "The meta description of the page to create" - ), - page_parent_folder_id: z - .string() - .optional() - .describe( - "The id of the parent page folder to create the page in" - ), - }) - .optional() - .describe("Create new page"), - create_page_folder: z - .object({ - page_folder_name: z - .string() - .describe( - "The name of the page folder to create" - ), - page_folder_parent_id: z - .string() - .optional() - .describe( - "The id of the parent page folder to create the page folder in" - ), - }) - .optional() - .describe("Create new page folder"), + title: "Designer Page Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Designer Tool - Page tool to perform actions like create page, create page folder, get current page, switch page", + inputSchema: { + ...SiteIdSchema, + actions: z.array( + z.object({ + create_page: z + .object({ + page_name: z + .string() + .describe("The name of the page to create"), + meta_title: z + .string() + .describe("The meta title of the page to create"), + meta_description: z + .string() + .optional() + .describe("The meta description of the page to create"), + page_parent_folder_id: z + .string() + .optional() + .describe( + "The id of the parent page folder to create the page in" + ), + }) + .optional() + .describe("Create new page"), + create_page_folder: z + .object({ + page_folder_name: z + .string() + .describe("The name of the page folder to create"), + page_folder_parent_id: z + .string() + .optional() + .describe( + "The id of the parent page folder to create the page folder in" + ), + }) + .optional() + .describe("Create new page folder"), - get_current_page: z - .boolean() - .optional() - .describe( - "Get current page active on webflow designer" - ), - switch_page: z - .object({ - page_id: z - .string() - .describe( - "The id of the page to switch to" - ), - }) - .optional() - .describe( - "Switch to a page on webflow designer" - ), - }) - ), + get_current_page: z + .boolean() + .optional() + .describe("Get current page active on webflow designer"), + switch_page: z + .object({ + page_id: z.string().describe("The id of the page to switch to"), + }) + .optional() + .describe("Switch to a page on webflow designer"), + }) + ), + }, }, async ({ siteId, actions }) => { try { - return formatResponse( - await pageToolRPCCall(siteId, actions) - ); + return formatResponse(await pageToolRPCCall(siteId, actions)); } catch (error) { return formatErrorResponse(error); } diff --git a/src/tools/deStyle.ts b/src/tools/deStyle.ts index c7914c0..33a66ae 100644 --- a/src/tools/deStyle.ts +++ b/src/tools/deStyle.ts @@ -2,195 +2,175 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { RPCType } from "../types/RPCType"; import z from "zod"; import { SiteIdSchema } from "../schemas"; -import { - formatErrorResponse, - formatResponse, - supportDEStyles, -} from "../utils"; +import { formatErrorResponse, formatResponse, supportDEStyles } from "../utils"; -export function registerDEStyleTools( - server: McpServer, - rpc: RPCType -) { - const styleToolRPCCall = async ( - siteId: string, - actions: any - ) => { +export function registerDEStyleTools(server: McpServer, rpc: RPCType) { + const styleToolRPCCall = async (siteId: string, actions: any) => { return rpc.callTool("style_tool", { siteId, actions: actions || [], }); }; - server.tool( + server.registerTool( "style_tool", - "Designer Tool - Style tool to perform actions like create style, get all styles, update styles", { - ...SiteIdSchema, - actions: z.array( - z.object({ - create_style: z - .object({ - name: z - .string() - .describe("The name of the style"), - properties: z - .array( - z.object({ - property_name: z - .string() - .describe("The name of the property"), - property_value: z - .string() - .optional() - .describe( - "The value of the property" - ), - variable_as_value: z - .string() - .optional() - .describe( - "The variable id to use as the value" - ), - }) - ) - .describe( - "The properties of the style. if you are looking to link a variable as the value, then use the variable_as_value field. but do not use both property_value and variable_as_value" - ), - parent_style_name: z - .string() - .optional() - .describe( - "The name of the parent style to create the new style in. this will use to create combo class" - ), - }) - .optional() - .describe("Create a new style"), - get_styles: z - .object({ - skip_properties: z - .boolean() - .optional() - .describe( - "Whether to skip the properties of the style. to get minimal data." - ), - include_all_breakpoints: z - .boolean() - .optional() - .describe( - "Whether to include all breakpoints styles or not. very data intensive." - ), - query: z - .enum(["all", "filtered"]) - .describe( - "The query to get all styles or filtered styles" - ), - filter_ids: z - .array(z.string()) - .optional() - .describe( - "The ids of the styles to filter by. should be used with query filtered" - ), - }) - .optional() - .describe("Get all styles"), - update_style: z - .object({ - style_name: z - .string() - .describe( - "The name of the style to update" - ), - breakpoint_id: z - .enum([ - "xxl", - "xl", - "large", - "main", - "medium", - "small", - "tiny", - ]) - .optional() - .describe( - "The breakpoint to update the style for" - ), - pseudo: z - .enum([ - "noPseudo", - "nth-child(odd)", - "nth-child(even)", - "first-child", - "last-child", - "hover", - "active", - "pressed", - "visited", - "focus", - "focus-visible", - "focus-within", - "placeholder", - "empty", - "before", - "after", - ]) - .optional() - .describe( - "The pseudo class to update the style for" - ), - properties: z - .array( - z.object({ - property_name: z - .string() - .describe("The name of the property"), - property_value: z - .string() - .optional() - .describe( - "The value of the property" - ), - variable_as_value: z - .string() - .optional() - .describe( - "The variable id to use as the value" - ), - }) - ) - .optional() - .describe( - "The properties to update or add to the style for" - ), - remove_properties: z - .array(z.string()) - .optional() - .describe( - "The properties to remove from the style" - ), - }) - .optional() - .describe("Update a style"), - }) - ), + title: "Designer Style Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Designer Tool - Style tool to perform actions like create style, get all styles, update styles", + inputSchema: { + ...SiteIdSchema, + actions: z.array( + z.object({ + create_style: z + .object({ + name: z.string().describe("The name of the style"), + properties: z + .array( + z.object({ + property_name: z + .string() + .describe("The name of the property"), + property_value: z + .string() + .optional() + .describe("The value of the property"), + variable_as_value: z + .string() + .optional() + .describe("The variable id to use as the value"), + }) + ) + .describe( + "The properties of the style. if you are looking to link a variable as the value, then use the variable_as_value field. but do not use both property_value and variable_as_value" + ), + parent_style_name: z + .string() + .optional() + .describe( + "The name of the parent style to create the new style in. this will use to create combo class" + ), + }) + .optional() + .describe("Create a new style"), + get_styles: z + .object({ + skip_properties: z + .boolean() + .optional() + .describe( + "Whether to skip the properties of the style. to get minimal data." + ), + include_all_breakpoints: z + .boolean() + .optional() + .describe( + "Whether to include all breakpoints styles or not. very data intensive." + ), + query: z + .enum(["all", "filtered"]) + .describe("The query to get all styles or filtered styles"), + filter_ids: z + .array(z.string()) + .optional() + .describe( + "The ids of the styles to filter by. should be used with query filtered" + ), + }) + .optional() + .describe("Get all styles"), + update_style: z + .object({ + style_name: z + .string() + .describe("The name of the style to update"), + breakpoint_id: z + .enum([ + "xxl", + "xl", + "large", + "main", + "medium", + "small", + "tiny", + ]) + .optional() + .describe("The breakpoint to update the style for"), + pseudo: z + .enum([ + "noPseudo", + "nth-child(odd)", + "nth-child(even)", + "first-child", + "last-child", + "hover", + "active", + "pressed", + "visited", + "focus", + "focus-visible", + "focus-within", + "placeholder", + "empty", + "before", + "after", + ]) + .optional() + .describe("The pseudo class to update the style for"), + properties: z + .array( + z.object({ + property_name: z + .string() + .describe("The name of the property"), + property_value: z + .string() + .optional() + .describe("The value of the property"), + variable_as_value: z + .string() + .optional() + .describe("The variable id to use as the value"), + }) + ) + .optional() + .describe("The properties to update or add to the style for"), + remove_properties: z + .array(z.string()) + .optional() + .describe("The properties to remove from the style"), + }) + .optional() + .describe("Update a style"), + }) + ), + }, }, async ({ siteId, actions }) => { try { - return formatResponse( - await styleToolRPCCall(siteId, actions) - ); + return formatResponse(await styleToolRPCCall(siteId, actions)); } catch (error) { return formatErrorResponse(error); } } ); - server.tool( + server.registerTool( "de_learn_more_about_styles", - "Designer tool - Learn more about styles supported by Webflow Designer." + - "Please do not use any other styles which is not supported by Webflow Designer." + - "Please use the long-form alias of a CSS property when managing styles. For example, the property row-gap has a long-form alias of grid-row-gap, margin has long-form alias of margin-top, margin-right, margin-bottom, margin-left, etc.", - {}, + { + title: "Designer Learn More About Webflow Styles", + annotations: { + readOnlyHint: true, + }, + description: + "Designer tool - Learn more about styles supported by Webflow Designer." + + "Please do not use any other styles which is not supported by Webflow Designer." + + "Please use the long-form alias of a CSS property when managing styles. For example, the property row-gap has a long-form alias of grid-row-gap, margin has long-form alias of margin-top, margin-right, margin-bottom, margin-left, etc.", + inputSchema: {}, + }, async ({}) => formatResponse(supportDEStyles) ); } diff --git a/src/tools/deVariable.ts b/src/tools/deVariable.ts index ed04e8e..9849235 100644 --- a/src/tools/deVariable.ts +++ b/src/tools/deVariable.ts @@ -2,234 +2,214 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { RPCType } from "../types/RPCType"; import z from "zod"; import { SiteIdSchema } from "../schemas"; -import { - formatErrorResponse, - formatResponse, -} from "../utils"; +import { formatErrorResponse, formatResponse } from "../utils"; -export function registerDEVariableTools( - server: McpServer, - rpc: RPCType -) { - const variableToolRPCCall = async ( - siteId: string, - actions: any - ) => { +export function registerDEVariableTools(server: McpServer, rpc: RPCType) { + const variableToolRPCCall = async (siteId: string, actions: any) => { return rpc.callTool("variable_tool", { siteId, actions: actions || [], }); }; - server.tool( + server.registerTool( "variable_tool", - "Designer Tool - Variable tool to perform actions like create variable, get all variables, update variable", { - ...SiteIdSchema, - actions: z.array( - z.object({ - create_variable_collection: z - .object({ - name: z - .string() - .describe( - "The name of the variable collection to create" - ), - }) - .optional() - .describe("Create a new variable collection"), - create_variable_mode: z - .object({ - variable_collection_id: z - .string() - .describe( - "The id of the variable collection to create the variable mode in" - ), - name: z - .string() - .describe( - "The name of the variable mode to create" - ), - }) - .optional() - .describe( - "Create a new variable mode in a variable collection" - ), - get_variable_collections: z - .object({ - query: z - .enum(["all", "filtered"]) - .describe( - "The query to get all variable collections" - ), - filter_collections_by_ids: z - .array(z.string()) - .optional() - .describe( - "The ids of the variable collections to filter by" - ), - }) - .optional() - .describe( - "Get all variable collections and its modes" - ), - get_variables: z - .object({ - variable_collection_id: z - .string() - .describe( - "The id of the variable collection to get the variables from" - ), - include_all_modes: z - .boolean() - .optional() - .describe( - "Whether to include all modes or not" - ), - filter_variables_by_ids: z - .array(z.string()) - .optional() - .describe( - "The ids of the variables to filter by" - ), - }) - .optional() - .describe( - "Get all variables in a variable collection and its modes" - ), - create_color_variable: z - .object({ - variable_collection_id: z.string(), - variable_name: z.string(), - value: z.object({ - static_value: z.string().optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Create a new color variable"), - create_size_variable: z - .object({ - variable_collection_id: z.string(), - variable_name: z.string(), - value: z.object({ - static_value: z - .object({ - value: z.number(), - unit: z.string(), - }) - .optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Create a new size variable"), - create_number_variable: z - .object({ - variable_collection_id: z.string(), - variable_name: z.string(), - value: z.object({ - static_value: z.number().optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Create a new number variable"), - create_percentage_variable: z - .object({ - variable_collection_id: z.string(), - variable_name: z.string(), - value: z.object({ - static_value: z.number().optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Create a new percentage variable"), - create_font_family_variable: z - .object({ - variable_collection_id: z.string(), - variable_name: z.string(), - value: z.object({ - static_value: z.string().optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Create a new font family variable"), - update_color_variable: z - .object({ - variable_collection_id: z.string(), - variable_id: z.string(), - mode_id: z.string().optional(), - value: z.object({ - static_value: z.string().optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Update a color variable"), - update_size_variable: z - .object({ - variable_collection_id: z.string(), - variable_id: z.string(), - mode_id: z.string().optional(), - value: z.object({ - static_value: z - .object({ - value: z.number(), - unit: z.string(), - }) - .optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Update a size variable"), - update_number_variable: z - .object({ - variable_collection_id: z.string(), - variable_id: z.string(), - mode_id: z.string().optional(), - value: z.object({ - static_value: z.number().optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Update a number variable"), - update_percentage_variable: z - .object({ - variable_collection_id: z.string(), - variable_id: z.string(), - mode_id: z.string().optional(), - value: z.object({ - static_value: z.number().optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Update a percentage variable"), - update_font_family_variable: z - .object({ - variable_collection_id: z.string(), - variable_id: z.string(), - mode_id: z.string().optional(), - value: z.object({ - static_value: z.string().optional(), - existing_variable_id: z.string().optional(), - }), - }) - .optional() - .describe("Update a font family variable"), - }) - ), + title: "Designer Variable Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Designer Tool - Variable tool to perform actions like create variable, get all variables, update variable", + inputSchema: { + ...SiteIdSchema, + actions: z.array( + z.object({ + create_variable_collection: z + .object({ + name: z + .string() + .describe("The name of the variable collection to create"), + }) + .optional() + .describe("Create a new variable collection"), + create_variable_mode: z + .object({ + variable_collection_id: z + .string() + .describe( + "The id of the variable collection to create the variable mode in" + ), + name: z + .string() + .describe("The name of the variable mode to create"), + }) + .optional() + .describe("Create a new variable mode in a variable collection"), + get_variable_collections: z + .object({ + query: z + .enum(["all", "filtered"]) + .describe("The query to get all variable collections"), + filter_collections_by_ids: z + .array(z.string()) + .optional() + .describe("The ids of the variable collections to filter by"), + }) + .optional() + .describe("Get all variable collections and its modes"), + get_variables: z + .object({ + variable_collection_id: z + .string() + .describe( + "The id of the variable collection to get the variables from" + ), + include_all_modes: z + .boolean() + .optional() + .describe("Whether to include all modes or not"), + filter_variables_by_ids: z + .array(z.string()) + .optional() + .describe("The ids of the variables to filter by"), + }) + .optional() + .describe( + "Get all variables in a variable collection and its modes" + ), + create_color_variable: z + .object({ + variable_collection_id: z.string(), + variable_name: z.string(), + value: z.object({ + static_value: z.string().optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Create a new color variable"), + create_size_variable: z + .object({ + variable_collection_id: z.string(), + variable_name: z.string(), + value: z.object({ + static_value: z + .object({ + value: z.number(), + unit: z.string(), + }) + .optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Create a new size variable"), + create_number_variable: z + .object({ + variable_collection_id: z.string(), + variable_name: z.string(), + value: z.object({ + static_value: z.number().optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Create a new number variable"), + create_percentage_variable: z + .object({ + variable_collection_id: z.string(), + variable_name: z.string(), + value: z.object({ + static_value: z.number().optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Create a new percentage variable"), + create_font_family_variable: z + .object({ + variable_collection_id: z.string(), + variable_name: z.string(), + value: z.object({ + static_value: z.string().optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Create a new font family variable"), + update_color_variable: z + .object({ + variable_collection_id: z.string(), + variable_id: z.string(), + mode_id: z.string().optional(), + value: z.object({ + static_value: z.string().optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Update a color variable"), + update_size_variable: z + .object({ + variable_collection_id: z.string(), + variable_id: z.string(), + mode_id: z.string().optional(), + value: z.object({ + static_value: z + .object({ + value: z.number(), + unit: z.string(), + }) + .optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Update a size variable"), + update_number_variable: z + .object({ + variable_collection_id: z.string(), + variable_id: z.string(), + mode_id: z.string().optional(), + value: z.object({ + static_value: z.number().optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Update a number variable"), + update_percentage_variable: z + .object({ + variable_collection_id: z.string(), + variable_id: z.string(), + mode_id: z.string().optional(), + value: z.object({ + static_value: z.number().optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Update a percentage variable"), + update_font_family_variable: z + .object({ + variable_collection_id: z.string(), + variable_id: z.string(), + mode_id: z.string().optional(), + value: z.object({ + static_value: z.string().optional(), + existing_variable_id: z.string().optional(), + }), + }) + .optional() + .describe("Update a font family variable"), + }) + ), + }, }, async ({ siteId, actions }) => { try { - return formatResponse( - await variableToolRPCCall(siteId, actions) - ); + return formatResponse(await variableToolRPCCall(siteId, actions)); } catch (error) { return formatErrorResponse(error); } diff --git a/src/tools/localDeMCPConnection.ts b/src/tools/localDeMCPConnection.ts index 23b98af..80a6922 100644 --- a/src/tools/localDeMCPConnection.ts +++ b/src/tools/localDeMCPConnection.ts @@ -1,10 +1,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { RPCType } from "../types/RPCType"; -import { - formatErrorResponse, - formatResponse, -} from "../utils/formatResponse"; +import { formatErrorResponse, formatResponse } from "../utils/formatResponse"; export function registerLocalDeMCPConnectionTools( server: McpServer, rpc: RPCType @@ -13,15 +10,20 @@ export function registerLocalDeMCPConnectionTools( return rpc.callTool("local_de_mcp_connection_tool", {}); }; - server.tool( + server.registerTool( "get_designer_app_connection_info", - "Get Webflow MCP App Connection Info. if user ask to get Webflow MCP app connection info, use this tool", - {}, + { + title: "Get Webflow MCP App Connection Info", + annotations: { + readOnlyHint: true, + }, + description: + "Get Webflow MCP App Connection Info. if user ask to get Webflow MCP app connection info, use this tool", + inputSchema: {}, + }, async () => { try { - return formatResponse( - await localDeMCPConnectionToolRPCCall() - ); + return formatResponse(await localDeMCPConnectionToolRPCCall()); } catch (error) { return formatErrorResponse(error); } diff --git a/src/tools/pages.ts b/src/tools/pages.ts index 0d26ca6..4b37d81 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -99,116 +99,123 @@ export function registerPagesTools( return response; }; - server.tool( + server.registerTool( "data_pages_tool", - "Data tool - Pages tool to perform actions like list pages, get page metadata, update page settings, get page content, and update static content", { - actions: z.array( - z.object({ - // GET https://api.webflow.com/v2/sites/:site_id/pages - list_pages: z - .object({ - site_id: z - .string() - .describe("The site's unique ID, used to list its pages."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - limit: z - .number() - .optional() - .describe( - "Maximum number of records to be returned (max limit: 100)" - ), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }) - .optional() - .describe( - "List all pages within a site. Returns page metadata including IDs, titles, and slugs." - ), - // GET https://api.webflow.com/v2/pages/:page_id - get_page_metadata: z - .object({ - page_id: z.string().describe("Unique identifier for the page."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - }) - .optional() - .describe( - "Get metadata for a specific page including SEO settings, Open Graph data, and page status (draft/published)." - ), - // PUT https://api.webflow.com/v2/pages/:page_id - update_page_settings: z - .object({ - page_id: z.string().describe("Unique identifier for the page."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - body: WebflowPageSchema, - }) - .optional() - .describe( - "Update page settings including SEO metadata, Open Graph data, slug, and publishing status." - ), - // GET https://api.webflow.com/v2/pages/:page_id/dom - get_page_content: z - .object({ - page_id: z.string().describe("Unique identifier for the page."), - localeId: z - .string() - .optional() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - limit: z - .number() - .optional() - .describe( - "Maximum number of records to be returned (max limit: 100)" - ), - offset: z - .number() - .optional() - .describe( - "Offset used for pagination if the results have more than limit records." - ), - }) - .optional() - .describe( - "Get the content structure and data for a specific page including all elements and their properties for localization." - ), - // POST https://api.webflow.com/v2/pages/:page_id/dom - update_static_content: z - .object({ - page_id: z.string().describe("Unique identifier for the page."), - localeId: z - .string() - .describe( - "Unique identifier for a specific locale. Applicable when using localization." - ), - nodes: WebflowPageDomWriteNodesItemSchema, - }) - .optional() - .describe( - "Update content on a static page in secondary locales by modifying text nodes and property overrides." - ), - }) - ), + title: "Data Pages Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Data tool - Pages tool to perform actions like list pages, get page metadata, update page settings, get page content, and update static content", + inputSchema: { + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites/:site_id/pages + list_pages: z + .object({ + site_id: z + .string() + .describe("The site's unique ID, used to list its pages."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "List all pages within a site. Returns page metadata including IDs, titles, and slugs." + ), + // GET https://api.webflow.com/v2/pages/:page_id + get_page_metadata: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + }) + .optional() + .describe( + "Get metadata for a specific page including SEO settings, Open Graph data, and page status (draft/published)." + ), + // PUT https://api.webflow.com/v2/pages/:page_id + update_page_settings: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + body: WebflowPageSchema, + }) + .optional() + .describe( + "Update page settings including SEO metadata, Open Graph data, slug, and publishing status." + ), + // GET https://api.webflow.com/v2/pages/:page_id/dom + get_page_content: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + limit: z + .number() + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + }) + .optional() + .describe( + "Get the content structure and data for a specific page including all elements and their properties for localization." + ), + // POST https://api.webflow.com/v2/pages/:page_id/dom + update_static_content: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + localeId: z + .string() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + nodes: WebflowPageDomWriteNodesItemSchema, + }) + .optional() + .describe( + "Update content on a static page in secondary locales by modifying text nodes and property overrides." + ), + }) + ), + }, }, async ({ actions }) => { const result: Content[] = []; diff --git a/src/tools/rules.ts b/src/tools/rules.ts index 657042a..3dcbcdd 100644 --- a/src/tools/rules.ts +++ b/src/tools/rules.ts @@ -1,10 +1,18 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; export function registerRulesTools(server: McpServer) { - server.tool( + server.registerTool( "webflow_guide_tool", - "Provides essential guidelines and best practices for effectively using the Webflow tools. Call this tool to understand recommended workflows and important considerations before performing actions. ALWAYS CALL THIS TOOL FIRST BEFORE CALLING ANY OTHER TOOLS. ALWAYS CALL THIS TOOL FIRST BEFORE CALLING ANY OTHER TOOLS. ", - {}, + { + title: "Webflow Guide Tool", + annotations: { + readOnlyHint: true, + openWorldHint: false, + }, + description: + "Provides essential guidelines and best practices for effectively using the Webflow tools. Call this tool to understand recommended workflows and important considerations before performing actions. ALWAYS CALL THIS TOOL FIRST BEFORE CALLING ANY OTHER TOOLS. ALWAYS CALL THIS TOOL FIRST BEFORE CALLING ANY OTHER TOOLS. ", + inputSchema: {}, + }, async ({}) => ({ content: [ { diff --git a/src/tools/scripts.ts b/src/tools/scripts.ts index dcf6c2e..b40a92e 100644 --- a/src/tools/scripts.ts +++ b/src/tools/scripts.ts @@ -104,51 +104,58 @@ export function registerScriptsTools( } }; - server.tool( + server.registerTool( "data_scripts_tool", - "Data tool - Scripts tool to perform actions like list registered scripts, list applied scripts, add inline site script, and delete all site scripts", { - actions: z.array( - z.object({ - // GET https://api.webflow.com/v2/sites/:site_id/registered_scripts - list_registered_scripts: z - .object({ - site_id: z.string().describe("Unique identifier for the site."), - }) - .optional() - .describe( - "List all registered scripts for a site. To apply a script to a site or page, first register it via the Register Script endpoints, then apply it using the relevant Site or Page endpoints." - ), - // GET https://api.webflow.com/v2/sites/:site_id/custom_code - list_applied_scripts: z - .object({ - site_id: z.string().describe("Unique identifier for the site."), - }) - .optional() - .describe( - "Get all scripts applied to a site by the App. To apply a script to a site or page, first register it via the Register Script endpoints, then apply it using the relevant Site or Page endpoints." - ), - // POST https://api.webflow.com/v2/sites/:site_id/registered_scripts/inline - add_inline_site_script: z - .object({ - site_id: z.string().describe("Unique identifier for the site."), - request: RegisterInlineSiteScriptSchema, - }) - .optional() - .describe( - "Register an inline script for a site. Inline scripts are limited to 2000 characters." - ), - // DELETE https://api.webflow.com/v2/sites/:site_id/custom_code - delete_all_site_scripts: z - .object({ - site_id: z.string().describe("Unique identifier for the site."), - }) - .optional() - .describe( - "Delete all custom scripts applied to a site by the App." - ), - }) - ), + title: "Data Scripts Tool", + annotations: { + readOnlyHint: false, + }, + description: + "Data tool - Scripts tool to perform actions like list registered scripts, list applied scripts, add inline site script, and delete all site scripts", + inputSchema: { + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites/:site_id/registered_scripts + list_registered_scripts: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + }) + .optional() + .describe( + "List all registered scripts for a site. To apply a script to a site or page, first register it via the Register Script endpoints, then apply it using the relevant Site or Page endpoints." + ), + // GET https://api.webflow.com/v2/sites/:site_id/custom_code + list_applied_scripts: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + }) + .optional() + .describe( + "Get all scripts applied to a site by the App. To apply a script to a site or page, first register it via the Register Script endpoints, then apply it using the relevant Site or Page endpoints." + ), + // POST https://api.webflow.com/v2/sites/:site_id/registered_scripts/inline + add_inline_site_script: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + request: RegisterInlineSiteScriptSchema, + }) + .optional() + .describe( + "Register an inline script for a site. Inline scripts are limited to 2000 characters." + ), + // DELETE https://api.webflow.com/v2/sites/:site_id/custom_code + delete_all_site_scripts: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + }) + .optional() + .describe( + "Delete all custom scripts applied to a site by the App." + ), + }) + ), + }, }, async ({ actions }) => { const result: Content[] = []; diff --git a/src/tools/sites.ts b/src/tools/sites.ts index ca8840c..e8d8b67 100644 --- a/src/tools/sites.ts +++ b/src/tools/sites.ts @@ -39,47 +39,50 @@ export function registerSiteTools( return response; }; - server.tool( + server.registerTool( "data_sites_tool", - "Data tool - Sites tool to perform actions like list sites, get site details, and publish sites", { - actions: z.array( - z.object({ - // GET https://api.webflow.com/v2/sites - list_sites: z - .object({}) - .optional() - .describe( - "List all sites accessible to the authenticated user. Returns basic site information including site ID, name, and last published date." - ), - // GET https://api.webflow.com/v2/sites/:site_id - get_site: z - .object({ - site_id: z.string().describe("Unique identifier for the site."), - }) - .optional() - .describe( - "Get detailed information about a specific site including its settings, domains, and publishing status." - ), - // POST https://api.webflow.com/v2/sites/:site_id/publish - publish_site: z - .object({ - site_id: z.string().describe("Unique identifier for the site."), - customDomains: z - .array(z.string()) - .optional() - .describe("Array of custom domains to publish the site to."), - publishToWebflowSubdomain: z - .boolean() - .optional() - .describe("Whether to publish to the Webflow subdomain."), - }) - .optional() - .describe( - "Publish a site to specified domains. This will make the latest changes live on the specified domains." - ), - }) - ), + description: + "Data tool - Sites tool to perform actions like list sites, get site details, and publish sites", + inputSchema: { + actions: z.array( + z.object({ + // GET https://api.webflow.com/v2/sites + list_sites: z + .object({}) + .optional() + .describe( + "List all sites accessible to the authenticated user. Returns basic site information including site ID, name, and last published date." + ), + // GET https://api.webflow.com/v2/sites/:site_id + get_site: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + }) + .optional() + .describe( + "Get detailed information about a specific site including its settings, domains, and publishing status." + ), + // POST https://api.webflow.com/v2/sites/:site_id/publish + publish_site: z + .object({ + site_id: z.string().describe("Unique identifier for the site."), + customDomains: z + .array(z.string()) + .optional() + .describe("Array of custom domains to publish the site to."), + publishToWebflowSubdomain: z + .boolean() + .optional() + .describe("Whether to publish to the Webflow subdomain."), + }) + .optional() + .describe( + "Publish a site to specified domains. This will make the latest changes live on the specified domains." + ), + }) + ), + }, }, async ({ actions }) => { const result: Content[] = []; From 8ca531a9dffd8bbc2e382a6a2b8044c21edb82aa Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Thu, 27 Nov 2025 03:54:59 +0530 Subject: [PATCH 10/14] chore: update @modelcontextprotocol/sdk to version 1.23.0 and enhance tools registration - Updated the @modelcontextprotocol/sdk dependency in package.json to version 1.23.0. - Added registration for new tools: registerCommentsTools and registerEnterpriseTools in mcp.ts. - Exported new tools from index.ts for better modularity. - Enhanced the rules.ts file with additional guidance on localizations. - Introduced new script management functions in scripts.ts for handling custom scripts on pages. --- package.json | 2 +- src/mcp.ts | 4 + src/tools/comments.ts | 286 ++++++++++++++++++++++++ src/tools/enterprise.ts | 482 ++++++++++++++++++++++++++++++++++++++++ src/tools/index.ts | 2 + src/tools/rules.ts | 1 + src/tools/scripts.ts | 104 +++++++++ 7 files changed, 880 insertions(+), 1 deletion(-) create mode 100644 src/tools/comments.ts create mode 100644 src/tools/enterprise.ts diff --git a/package.json b/package.json index 55d155f..55d7996 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build:watch": "tsup src/index.ts --dts --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "1.21.1", + "@modelcontextprotocol/sdk": "1.23.0", "cors": "^2.8.5", "express": "^5.1.0", "socket.io": "^4.8.1", diff --git a/src/mcp.ts b/src/mcp.ts index 701eaaf..a80e4ff 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -15,6 +15,8 @@ import { registerDEVariableTools, registerRulesTools, registerLocalDeMCPConnectionTools, + registerCommentsTools, + registerEnterpriseTools, } from "./tools"; import { RPCType } from "./types/RPCType"; @@ -51,6 +53,8 @@ export function registerTools( registerPagesTools(server, getClient); registerScriptsTools(server, getClient); registerSiteTools(server, getClient); + registerCommentsTools(server, getClient); + registerEnterpriseTools(server, getClient); } export function registerDesignerTools(server: McpServer, rpc: RPCType) { diff --git a/src/tools/comments.ts b/src/tools/comments.ts new file mode 100644 index 0000000..e095caf --- /dev/null +++ b/src/tools/comments.ts @@ -0,0 +1,286 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { WebflowClient } from "webflow-api"; +import z from "zod"; +import { + Content, + formatErrorResponse, + textContent, + toolResponse, +} from "../utils/formatResponse"; +import { requestOptions } from "../mcp"; +import { + CommentsGetCommentThreadRequest, + CommentsListCommentRepliesRequest, + CommentsListCommentThreadsRequest, +} from "webflow-api/api/resources/sites"; + +export function registerCommentsTools( + server: McpServer, + getClient: () => WebflowClient +) { + const listCommentThreads = async (arg: { + site_id: string; + localeId?: string; + offset?: number; + limit?: number; + sortBy?: "createdOn" | "lastUpdated"; + sortOrder?: "asc" | "desc"; + }) => { + const data: CommentsListCommentThreadsRequest = {}; + if ("localeId" in arg) { + data.localeId = arg.localeId; + } + if ("offset" in arg) { + data.offset = arg.offset; + } + if ("limit" in arg) { + data.limit = arg.limit; + } + if ("sortBy" in arg) { + data.sortBy = arg.sortBy; + } + if ("sortOrder" in arg) { + data.sortOrder = arg.sortOrder; + } + const response = await getClient().sites.comments.listCommentThreads( + arg.site_id, + data, + requestOptions + ); + return response; + }; + + const getCommentThread = async (arg: { + site_id: string; + comment_thread_id: string; + localeId?: string; + offset?: number; + limit?: number; + sortBy?: "createdOn" | "lastUpdated"; + sortOrder?: "asc" | "desc"; + }) => { + const data: CommentsGetCommentThreadRequest = {}; + if ("localeId" in arg) { + data.localeId = arg.localeId; + } + if ("offset" in arg) { + data.offset = arg.offset; + } + if ("limit" in arg) { + data.limit = arg.limit; + } + if ("sortBy" in arg) { + data.sortBy = arg.sortBy; + } + if ("sortOrder" in arg) { + data.sortOrder = arg.sortOrder; + } + const response = await getClient().sites.comments.getCommentThread( + arg.site_id, + arg.comment_thread_id, + data, + requestOptions + ); + return response; + }; + + const listCommentReplies = async (arg: { + site_id: string; + comment_thread_id: string; + localeId?: string; + offset?: number; + limit?: number; + sortBy?: "createdOn" | "lastUpdated"; + sortOrder?: "asc" | "desc"; + }) => { + const data: CommentsListCommentRepliesRequest = {}; + if ("localeId" in arg) { + data.localeId = arg.localeId; + } + if ("offset" in arg) { + data.offset = arg.offset; + } + if ("limit" in arg) { + data.limit = arg.limit; + } + if ("sortBy" in arg) { + data.sortBy = arg.sortBy; + } + if ("sortOrder" in arg) { + data.sortOrder = arg.sortOrder; + } + const response = await getClient().sites.comments.listCommentReplies( + arg.site_id, + arg.comment_thread_id, + data, + requestOptions + ); + return response; + }; + + server.registerTool( + "data_comments_tool", + { + title: "Data Comments Tool", + description: `Data tool - A comment in Webflow is user feedback attached to a specific element or page inside the Designer, stored as a top-level thread with optional replies. Each comment includes author info, timestamps, content, resolved state, and design-context metadata like page location and breakpoint. Use this tool to inspect feedback discussions across the site and understand where and why they were left.`, + annotations: { + readOnlyHint: true, + }, + inputSchema: { + actions: z + .array( + z.object({ + list_comment_threads: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to list its comment threads." + ), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + limit: z + .number() + .max(100) + .min(1) + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + sortBy: z + .enum(["createdOn", "lastUpdated"]) + .optional() + .describe("Sort the results by the given field."), + sortOrder: z + .enum(["asc", "desc"]) + .optional() + .describe("Sort the results by the given order."), + }) + .optional() + .describe( + "List all comment threads for a specific element or page." + ), + get_comment_thread: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to get its comment thread." + ), + comment_thread_id: z + .string() + .describe( + "The comment thread's unique ID, used to get its details." + ), + localeId: z + .string() + .optional() + .describe( + "Unique identifier for a specific locale. Applicable when using localization." + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + limit: z + .number() + .max(100) + .min(1) + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + sortBy: z + .enum(["createdOn", "lastUpdated"]) + .optional() + .describe("Sort the results by the given field."), + sortOrder: z + .enum(["asc", "desc"]) + .optional() + .describe("Sort the results by the given order."), + }) + .optional() + .describe("Get the details of a specific comment thread."), + list_comment_replies: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to list its comment replies." + ), + comment_thread_id: z + .string() + .describe( + "The comment thread's unique ID, used to list its replies." + ), + offset: z + .number() + .optional() + .describe( + "Offset used for pagination if the results have more than limit records." + ), + limit: z + .number() + .max(100) + .min(1) + .optional() + .describe( + "Maximum number of records to be returned (max limit: 100)" + ), + sortBy: z + .enum(["createdOn", "lastUpdated"]) + .optional() + .describe("Sort the results by the given field."), + sortOrder: z + .enum(["asc", "desc"]) + .optional() + .describe("Sort the results by the given order."), + }) + .optional() + .describe("List all replies for a specific comment thread."), + }) + ) + .min(1) + .describe("The actions to perform on the comments."), + }, + }, + async ({ actions }) => { + const result: Content[] = []; + try { + for (const action of actions) { + if (action.list_comment_threads) { + const content = await listCommentThreads( + action.list_comment_threads + ); + result.push(textContent(content)); + } + if (action.get_comment_thread) { + const content = await getCommentThread(action.get_comment_thread); + result.push(textContent(content)); + } + if (action.list_comment_replies) { + const content = await listCommentReplies( + action.list_comment_replies + ); + result.push(textContent(content)); + } + } + return toolResponse(result); + } catch (error) { + return formatErrorResponse(error); + } + } + ); +} diff --git a/src/tools/enterprise.ts b/src/tools/enterprise.ts new file mode 100644 index 0000000..9826019 --- /dev/null +++ b/src/tools/enterprise.ts @@ -0,0 +1,482 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { WebflowClient } from "webflow-api"; +import z from "zod"; +import { requestOptions } from "../mcp"; +import { Robots } from "webflow-api/api"; +import { + Content, + formatErrorResponse, + textContent, + toolResponse, +} from "../utils/formatResponse"; + +export function registerEnterpriseTools( + server: McpServer, + getClient: () => WebflowClient +) { + const list301Redirects = async (arg: { site_id: string }) => { + const response = await getClient().sites.redirects.list( + arg.site_id, + requestOptions + ); + return response; + }; + const create301Redirect = async (arg: { + site_id: string; + fromUrl: string; + toUrl: string; + }) => { + const response = await getClient().sites.redirects.create( + arg.site_id, + { + fromUrl: arg.fromUrl, + toUrl: arg.toUrl, + }, + requestOptions + ); + return response; + }; + const update301Redirect = async (arg: { + site_id: string; + redirect_id: string; + fromUrl: string; + toUrl: string; + }) => { + const response = await getClient().sites.redirects.update( + arg.site_id, + arg.redirect_id, + { + fromUrl: arg.fromUrl, + toUrl: arg.toUrl, + }, + requestOptions + ); + return response; + }; + const delete301Redirect = async (arg: { + site_id: string; + redirect_id: string; + }) => { + const response = await getClient().sites.redirects.delete( + arg.site_id, + arg.redirect_id, + requestOptions + ); + return response; + }; + const getRobotsDotTxt = async (arg: { site_id: string }) => { + const response = await getClient().sites.robotsTxt.get( + arg.site_id, + requestOptions + ); + return response; + }; + const updateRobotsDotTxt = async (arg: { + site_id: string; + rules?: { + userAgent: string; + allow: string[]; + disallow: string[]; + }[]; + sitemap?: string; + }) => { + const data: Robots = {}; + if (arg.rules) { + data.rules = arg.rules; + } + if (arg.sitemap) { + data.sitemap = arg.sitemap; + } + const response = await getClient().sites.robotsTxt.patch( + arg.site_id, + data, + requestOptions + ); + return response; + }; + const replaceRobotsDotTxt = async (arg: { + site_id: string; + rules?: { + userAgent: string; + allow: string[]; + disallow: string[]; + }[]; + sitemap?: string; + }) => { + const data: Robots = {}; + if (arg.rules) { + data.rules = arg.rules; + } + if (arg.sitemap) { + data.sitemap = arg.sitemap; + } + const response = await getClient().sites.robotsTxt.put( + arg.site_id, + data, + requestOptions + ); + return response; + }; + const deleteRobotsDotTxt = async (arg: { + site_id: string; + rules?: { + userAgent: string; + allow: string[]; + disallow: string[]; + }[]; + sitemap?: string; + }) => { + const data: Robots = {}; + if (arg.rules) { + data.rules = arg.rules; + } + if (arg.sitemap) { + data.sitemap = arg.sitemap; + } + const response = await getClient().sites.robotsTxt.patch( + arg.site_id, + data, + requestOptions + ); + return response; + }; + + const addWellKnownFile = async (arg: { + site_id: string; + fileName: string; + fileData: string; + contentType: "application/json" | "text/plain"; + }) => { + const response = await getClient().sites.wellKnown.put( + arg.site_id, + { + fileData: arg.fileData, + fileName: arg.fileName, + contentType: arg.contentType, + }, + requestOptions + ); + return response; + }; + + const removeWellKnownFiles = async (arg: { + site_id: string; + fileNames: string[]; + }) => { + const response = await getClient().sites.wellKnown.delete( + arg.site_id, + { + fileNames: arg.fileNames, + }, + requestOptions + ); + return response; + }; + + server.registerTool( + "data_enterprise_tool", + { + title: "Data Enterprise Tool", + description: + "Data tool - Enterprise tool to perform actions like manage 301 redirects, manage robots.txt and more. This tool only works if User's workspace plan is Enterprise or higher, else tool will return an error.", + annotations: { + readOnlyHint: false, + }, + inputSchema: { + actions: z + .array( + z.object({ + list_301_redirects: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to list its 301 redirects." + ), + }) + .optional() + .describe("List all 301 redirects for a site."), + create_301_redirect: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to create a 301 redirect." + ), + fromUrl: z + .string() + .describe( + "The source URL path that will be redirected (e.g., '/old-page')." + ), + toUrl: z + .string() + .describe( + "The destination URL path where requests will be redirected to (e.g., '/new-page')." + ), + }) + .optional() + .describe("Create a new 301 redirect for a site."), + update_301_redirect: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to update a 301 redirect." + ), + redirect_id: z + .string() + .describe( + "The redirect's unique ID, used to identify which redirect to update." + ), + fromUrl: z + .string() + .describe( + "The source URL path that will be redirected (e.g., '/old-page')." + ), + toUrl: z + .string() + .describe( + "The destination URL path where requests will be redirected to (e.g., '/new-page')." + ), + }) + .optional() + .describe("Update an existing 301 redirect."), + delete_301_redirect: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to delete a 301 redirect." + ), + redirect_id: z + .string() + .describe( + "The redirect's unique ID, used to identify which redirect to delete." + ), + }) + .optional() + .describe("Delete a 301 redirect from a site."), + get_robots_txt: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to get its robots.txt configuration." + ), + }) + .optional() + .describe("Get the robots.txt configuration for a site."), + update_robots_txt: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to update its robots.txt." + ), + rules: z + .array( + z.object({ + userAgent: z + .string() + .describe( + "The user agent to apply rules to (e.g., '*', 'Googlebot')." + ), + allow: z + .array(z.string()) + .describe("Array of URL paths to allow."), + disallow: z + .array(z.string()) + .describe("Array of URL paths to disallow."), + }) + ) + .optional() + .describe( + "Array of rules to apply to the robots.txt file." + ), + sitemap: z + .string() + .optional() + .describe( + "URL to the sitemap (e.g., 'https://example.com/sitemap.xml')." + ), + }) + .optional() + .describe( + "Partially update the robots.txt file (PATCH operation)." + ), + replace_robots_txt: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to replace its robots.txt." + ), + rules: z + .array( + z.object({ + userAgent: z + .string() + .describe( + "The user agent to apply rules to (e.g., '*', 'Googlebot')." + ), + allow: z + .array(z.string()) + .describe("Array of URL paths to allow."), + disallow: z + .array(z.string()) + .describe("Array of URL paths to disallow."), + }) + ) + .optional() + .describe( + "Array of rules to apply to the robots.txt file." + ), + sitemap: z + .string() + .optional() + .describe( + "URL to the sitemap (e.g., 'https://example.com/sitemap.xml')." + ), + }) + .optional() + .describe( + "Completely replace the robots.txt file (PUT operation)." + ), + delete_robots_txt: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to delete rules from its robots.txt." + ), + rules: z + .array( + z.object({ + userAgent: z + .string() + .describe( + "The user agent to apply rules to (e.g., '*', 'Googlebot')." + ), + allow: z + .array(z.string()) + .describe("Array of URL paths to allow."), + disallow: z + .array(z.string()) + .describe("Array of URL paths to disallow."), + }) + ) + .optional() + .describe( + "Array of rules to remove from the robots.txt file." + ), + sitemap: z + .string() + .optional() + .describe("Sitemap URL to remove."), + }) + .optional() + .describe("Delete specific rules from the robots.txt file."), + add_well_known_file: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to add a well-known file." + ), + fileName: z + .string() + .describe( + `The name of the well-known file (e.g., 'apple-app-site-association', 'assetlinks.json'). ".noext" is a special file extension that removes other extensions. For example, apple-app-site-association.noext.txt will be uploaded as apple-app-site-association. Use this extension for tools that have trouble uploading extensionless files.` + ), + fileData: z + .string() + .describe( + "The content/data of the well-known file as a string." + ), + contentType: z + .enum(["application/json", "text/plain"]) + .describe( + "The MIME type of the file content (application/json or text/plain)." + ), + }) + .optional() + .describe( + "Add or update a well-known file to the site's /.well-known/ directory." + ), + remove_well_known_files: z + .object({ + site_id: z + .string() + .describe( + "The site's unique ID, used to remove well-known files." + ), + fileNames: z + .array(z.string()) + .describe( + "Array of file names to remove from the /.well-known/ directory." + ), + }) + .optional() + .describe("Remove one or more well-known files from the site."), + }) + ) + .min(1) + .describe("The actions to perform on the enterprise tool."), + }, + }, + async ({ actions }) => { + const result: Content[] = []; + try { + for (const action of actions) { + if (action.list_301_redirects) { + const content = await list301Redirects(action.list_301_redirects); + result.push(textContent(content)); + } + if (action.create_301_redirect) { + const content = await create301Redirect(action.create_301_redirect); + result.push(textContent(content)); + } + if (action.update_301_redirect) { + const content = await update301Redirect(action.update_301_redirect); + result.push(textContent(content)); + } + if (action.delete_301_redirect) { + const content = await delete301Redirect(action.delete_301_redirect); + result.push(textContent(content)); + } + if (action.get_robots_txt) { + const content = await getRobotsDotTxt(action.get_robots_txt); + result.push(textContent(content)); + } + if (action.update_robots_txt) { + const content = await updateRobotsDotTxt(action.update_robots_txt); + result.push(textContent(content)); + } + if (action.replace_robots_txt) { + const content = await replaceRobotsDotTxt( + action.replace_robots_txt + ); + result.push(textContent(content)); + } + if (action.delete_robots_txt) { + const content = await deleteRobotsDotTxt(action.delete_robots_txt); + result.push(textContent(content)); + } + if (action.add_well_known_file) { + const content = await addWellKnownFile(action.add_well_known_file); + result.push(textContent(content)); + } + if (action.remove_well_known_files) { + const content = await removeWellKnownFiles( + action.remove_well_known_files + ); + result.push(textContent(content)); + } + } + return toolResponse(result); + } catch (error) { + return formatErrorResponse(error); + } + } + ); +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 0b39fdb..1c57d6b 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -5,6 +5,8 @@ export { registerComponentsTools } from "./components"; export { registerPagesTools } from "./pages"; export { registerScriptsTools } from "./scripts"; export { registerSiteTools } from "./sites"; +export { registerCommentsTools } from "./comments"; +export { registerEnterpriseTools } from "./enterprise"; // Designer API Tools export { registerDEAssetTools } from "./deAsset"; export { registerDEComponentsTools } from "./deComponents"; diff --git a/src/tools/rules.ts b/src/tools/rules.ts index 3dcbcdd..9d43b8d 100644 --- a/src/tools/rules.ts +++ b/src/tools/rules.ts @@ -29,6 +29,7 @@ export function registerRulesTools(server: McpServer) { `-- After updating or creating an element, the updated/created element is not automatically selected. If you need more information about that element, use element_tool > select_element with the appropriate element ID to select and inspect it.\n` + `-- Do not use CSS shorthand properties when updating or creating styles. Always use longhand property names like "margin-top", "padding-left", "border-width", etc.\n` + `-- When creating or updating elements, most users prefer using existing styles. You should reuse styles if they exist, unless the user explicitly wants new ones.\n` + + `-- To learn or find about localizations and locale id you get use site too and get site details to learn how many locales are supported and their details.\n` + `\n` + `Element Tool Usage:\n` + `-- To get detailed information about the currently selected element, use element_tool > get_selected_element.\n` + diff --git a/src/tools/scripts.ts b/src/tools/scripts.ts index b40a92e..2ec8d02 100644 --- a/src/tools/scripts.ts +++ b/src/tools/scripts.ts @@ -11,6 +11,7 @@ import { toolResponse, isApiError, } from "../utils"; +import { ScriptApplyList } from "webflow-api/api"; export function registerScriptsTools( server: McpServer, @@ -104,6 +105,43 @@ export function registerScriptsTools( } }; + const getPageScript = async (arg: { page_id: string }) => { + const response = await getClient().pages.scripts.getCustomCode( + arg.page_id, + requestOptions + ); + return response; + }; + + const upsertPageScript = async (arg: { + page_id: string; + scripts: { + id: string; + location: "header" | "footer"; + version: string; + attributes?: Record; + }[]; + }) => { + const data: ScriptApplyList = { + scripts: arg.scripts, + }; + + const response = await getClient().pages.scripts.upsertCustomCode( + arg.page_id, + data, + requestOptions + ); + return response; + }; + + const deleteAllPageScripts = async (arg: { page_id: string }) => { + const response = await getClient().pages.scripts.deleteCustomCode( + arg.page_id, + requestOptions + ); + return response; + }; + server.registerTool( "data_scripts_tool", { @@ -153,6 +191,58 @@ export function registerScriptsTools( .describe( "Delete all custom scripts applied to a site by the App." ), + // GET https://api.webflow.com/v2/pages/:page_id/custom_code + get_page_script: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + }) + .optional() + .describe( + "Get all custom scripts applied to a specific page by the App." + ), + // PUT https://api.webflow.com/v2/pages/:page_id/custom_code + upsert_page_script: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + scripts: z + .array( + z.object({ + id: z + .string() + .describe( + "The unique identifier of the registered script." + ), + location: z + .enum(["header", "footer"]) + .describe( + "The location where the script should be applied (header or footer)." + ), + version: z + .string() + .describe("The version of the script to apply."), + attributes: z + .record(z.any()) + .optional() + .describe( + "Optional attributes to apply to the script element." + ), + }) + ) + .describe("Array of scripts to apply to the page."), + }) + .optional() + .describe( + "Add or update custom scripts on a specific page. This will replace all existing scripts on the page with the provided scripts." + ), + // DELETE https://api.webflow.com/v2/pages/:page_id/custom_code + delete_all_page_scripts: z + .object({ + page_id: z.string().describe("Unique identifier for the page."), + }) + .optional() + .describe( + "Delete all custom scripts applied to a specific page by the App." + ), }) ), }, @@ -185,6 +275,20 @@ export function registerScriptsTools( ); result.push(textContent(content)); } + if (action.get_page_script) { + const content = await getPageScript(action.get_page_script); + result.push(textContent(content)); + } + if (action.upsert_page_script) { + const content = await upsertPageScript(action.upsert_page_script); + result.push(textContent(content)); + } + if (action.delete_all_page_scripts) { + const content = await deleteAllPageScripts( + action.delete_all_page_scripts + ); + result.push(textContent(content)); + } } return toolResponse(result); } catch (error) { From f950924574604865d5e0beec1c59f46414d9cb07 Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Thu, 27 Nov 2025 04:52:15 +0530 Subject: [PATCH 11/14] chore: simplify build scripts in package.json - Removed the --clean flag from the build script for a more straightforward build process. - Maintained the watch functionality in the build:watch script for continuous development. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 55d7996..477dc99 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "start": "concurrently \"npm run dev:local\" \"npm run inspector:local\"", "dev:local": "npm run build:watch", "inspector:local": "npx @modelcontextprotocol/inspector -- nodemon --env-file=.env -q --watch dist dist/index.js", - "build": "tsup src/index.ts --dts --clean", - "build:watch": "tsup src/index.ts --dts --watch" + "build": "tsup src/index.ts", + "build:watch": "tsup src/index.ts --watch" }, "dependencies": { "@modelcontextprotocol/sdk": "1.23.0", From ebe82953049d1bbad29453082dc40187c310980a Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Tue, 16 Dec 2025 20:23:59 +0530 Subject: [PATCH 12/14] feat: add element snapshot tool for visual debugging - Introduced the element_snapshot_tool to capture visual snapshots of elements, sections, or components. - Enhanced the rules documentation with usage instructions for the new tool, emphasizing its utility for debugging and visual validation. - Implemented RPC call handling for the snapshot tool, returning PNG images for specified elements. --- src/tools/deElement.ts | 61 ++++++++++++++++++++++++++++++++++++++++++ src/tools/rules.ts | 7 +++++ 2 files changed, 68 insertions(+) diff --git a/src/tools/deElement.ts b/src/tools/deElement.ts index 4605a68..a48799d 100644 --- a/src/tools/deElement.ts +++ b/src/tools/deElement.ts @@ -19,6 +19,27 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { }); }; + const elementSnapshotToolRPCCall = async ( + siteId: string, + action: any + ): Promise< + | { + status: string; + message: string; + data: null; + } + | { + status: string; + message: string; + data: string; + } + > => { + return rpc.callTool("element_snapshot_tool", { + siteId, + action: action || {}, + }); + }; + server.registerTool( "element_builder", { @@ -229,4 +250,44 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { } } ); + + server.registerTool( + "element_snapshot_tool", + { + annotations: { + readOnlyHint: true, + openWorldHint: true, + }, + description: + "Designer Tool - Element snapshot tool to perform actions like get element snapshot. helpful to get element snapshot for debugging and more and visual feedback.", + inputSchema: { + ...SiteIdSchema, + action: z.object({ + id: DEElementIDSchema.id, + }), + }, + }, + async ({ action, siteId }) => { + try { + const { status, message, data } = await elementSnapshotToolRPCCall( + siteId, + action + ); + if (status === "success" && data) { + return { + content: [ + { + type: "image", + data, + mimeType: "image/png", + }, + ], + }; + } + return formatErrorResponse(new Error(message)); + } catch (error) { + return formatErrorResponse(error); + } + } + ); }; diff --git a/src/tools/rules.ts b/src/tools/rules.ts index 9d43b8d..c007fdb 100644 --- a/src/tools/rules.ts +++ b/src/tools/rules.ts @@ -46,6 +46,13 @@ export function registerRulesTools(server: McpServer) { `Element Builder Tool:\n` + `-- To create a new element, use element_builder. Pass the type of element you want to create. After creation, use element_tool > select_element to select the element and gather additional details if needed.\n` + `\n` + + `Element Snapshot Tool Usage:\n` + + `-- To get a visual snapshot of an element, section, or component, use element_snapshot_tool. Pass the element ID to capture its current visual state as an image.\n` + + `-- Use this tool to verify visual changes after creating or updating elements. It provides immediate visual feedback without requiring manual inspection.\n` + + `-- This tool is helpful for debugging layout issues, verifying styling changes, or confirming that elements render as expected.\n` + + `-- The snapshot returns a PNG image of the specified element. Use it to validate your work before proceeding with additional changes.\n` + + `-- When the user asks to see or preview an element, use this tool to provide visual confirmation.\n` + + `\n` + `Asset Tool Usage:\n` + `-- To create an asset folder, use asset_tool > create_folder. Pass the name of the folder. To create a nested folder, pass parent_folder_id. Otherwise, the folder will be created in the root directory.\n` + `-- To retrieve assets and folders, use asset_tool > get_all_assets_and_folders. You can use query as "all", "folders", or "assets". To limit data, use filter_assets_by_ids or search query. Fetch only what you need to avoid context overload.\n` + From a558424c3bac266c6552f8c71454b3c8ac2fd19c Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Tue, 16 Dec 2025 21:45:06 +0530 Subject: [PATCH 13/14] fix: remove base64 prefix from image data in registerDEElementTools - Updated the image data handling in the registerDEElementTools function to remove the "data:image/png;base64," prefix, ensuring cleaner data processing for PNG images. --- src/tools/deElement.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/deElement.ts b/src/tools/deElement.ts index a48799d..8579936 100644 --- a/src/tools/deElement.ts +++ b/src/tools/deElement.ts @@ -278,7 +278,7 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { content: [ { type: "image", - data, + data: data.replace("data:image/png;base64,", ""), mimeType: "image/png", }, ], From dab4dda4cfefb5cb4dd7c3240580336734771b42 Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Sat, 17 Jan 2026 00:30:50 +0530 Subject: [PATCH 14/14] feat: enhance comments tool registration with openWorldHint - Added openWorldHint to the annotations in the registerCommentsTools function for improved user guidance. - Updated zod import to version 3 in enterprise.ts for consistency with other schema files. --- src/tools/comments.ts | 1 + src/tools/enterprise.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tools/comments.ts b/src/tools/comments.ts index e095caf..162e102 100644 --- a/src/tools/comments.ts +++ b/src/tools/comments.ts @@ -125,6 +125,7 @@ export function registerCommentsTools( description: `Data tool - A comment in Webflow is user feedback attached to a specific element or page inside the Designer, stored as a top-level thread with optional replies. Each comment includes author info, timestamps, content, resolved state, and design-context metadata like page location and breakpoint. Use this tool to inspect feedback discussions across the site and understand where and why they were left.`, annotations: { readOnlyHint: true, + openWorldHint: true, }, inputSchema: { actions: z diff --git a/src/tools/enterprise.ts b/src/tools/enterprise.ts index 9826019..b0c0c13 100644 --- a/src/tools/enterprise.ts +++ b/src/tools/enterprise.ts @@ -1,6 +1,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WebflowClient } from "webflow-api"; -import z from "zod"; +import z from "zod/v3"; import { requestOptions } from "../mcp"; import { Robots } from "webflow-api/api"; import {