From af50fc0cad65127f1d88c6aa4afbbf66ccd17333 Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Sat, 17 Jan 2026 01:31:49 +0530 Subject: [PATCH 1/3] refactor: improve page settings update logic in registerPagesTools - Changed the type of the body parameter to use z.infer with WebflowPageSchema for better type safety. - Extracted relevant fields (seo, openGraph, slug, title) from the body and constructed a new pageSettings object for cleaner updates. - Updated the updatePageSettings call to merge the new pageSettings with the existing body, ensuring a more structured request. --- src/tools/pages.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/tools/pages.ts b/src/tools/pages.ts index e3b0da1..171864a 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -52,13 +52,21 @@ export function registerPagesTools( const updatePageSettings = async (arg: { page_id: string; localeId?: string; - body: any; + body: z.infer; }) => { + const { seo, openGraph, slug, title, ...rest } = arg.body; + const pageSettings = { + ...(seo && { seo }), + ...(openGraph && { openGraph }), + ...(slug && { slug }), + ...(title && { title }), + }; const response = await getClient().pages.updatePageSettings( arg.page_id, { localeId: arg.localeId, - body: arg.body, + ...pageSettings, + body: { ...rest, ...pageSettings }, }, requestOptions ); From f53e470a511050a6e25d4ccc732cdbb006616de8 Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Mon, 16 Feb 2026 21:17:02 +0530 Subject: [PATCH 2/3] refactor: standardize input schemas and validation in various tools - Updated input schemas across multiple tools (cms, comments, components, deAsset, deComponents, deElement, dePages, deStyle) to ensure consistent structure and validation using zod. - Implemented strict validation and refined checks to enforce single action requirements in deAsset, deComponents, and dePages tools. - Enhanced descriptions and organization of schema properties for improved clarity and usability. --- src/tools/cms.ts | 493 ++++++++++++++++++++------------------ src/tools/comments.ts | 258 ++++++++++---------- src/tools/components.ts | 264 +++++++++++--------- src/tools/deAsset.ts | 110 +++++---- src/tools/deComponents.ts | 134 ++++++----- src/tools/deElement.ts | 264 ++++++++++---------- src/tools/dePages.ts | 111 +++++---- src/tools/deStyle.ts | 257 ++++++++++---------- src/tools/deVariable.ts | 389 ++++++++++++++++-------------- src/tools/enterprise.ts | 491 +++++++++++++++++++------------------ src/tools/scripts.ts | 214 ++++++++++------- src/tools/sites.ts | 87 ++++--- 12 files changed, 1667 insertions(+), 1405 deletions(-) diff --git a/src/tools/cms.ts b/src/tools/cms.ts index 701ef4d..6d4f164 100644 --- a/src/tools/cms.ts +++ b/src/tools/cms.ts @@ -217,242 +217,273 @@ export function registerCmsTools( "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({ + // 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." + ), + }) + .strict() + .refine( + (d) => + [ + d.get_collection_list, + d.get_collection_details, + d.create_collection, + d.create_collection_static_field, + d.create_collection_option_field, + d.create_collection_reference_field, + d.update_collection_field, + d.list_collection_items, + d.create_collection_items, + d.update_collection_items, + d.publish_collection_items, + d.delete_collection_items, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of get_collection_list, get_collection_details, create_collection, create_collection_static_field, create_collection_option_field, create_collection_reference_field, update_collection_field, list_collection_items, create_collection_items, update_collection_items, publish_collection_items, delete_collection_items.", + } + ) ), }, }, diff --git a/src/tools/comments.ts b/src/tools/comments.ts index 162e102..14d5570 100644 --- a/src/tools/comments.ts +++ b/src/tools/comments.ts @@ -130,128 +130,142 @@ export function registerCommentsTools( 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."), - }) + 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."), + }) + .strict() + .refine( + (d) => + [ + d.list_comment_threads, + d.get_comment_thread, + d.list_comment_replies, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of list_comment_threads, get_comment_thread, list_comment_replies.", + } + ) ) .min(1) .describe("The actions to perform on the comments."), diff --git a/src/tools/components.ts b/src/tools/components.ts index 5b8c851..8b230f5 100644 --- a/src/tools/components.ts +++ b/src/tools/components.ts @@ -121,125 +121,151 @@ export function registerComponentsTools( "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." - ), - }) + 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." + ), + }) + .strict() + .refine( + (d) => + [ + d.list_components, + d.get_component_content, + d.update_component_content, + d.get_component_properties, + d.update_component_properties, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of list_components, get_component_content, update_component_content, get_component_properties, update_component_properties.", + } + ) ), }, }, diff --git a/src/tools/deAsset.ts b/src/tools/deAsset.ts index a5d7e78..0014a50 100644 --- a/src/tools/deAsset.ts +++ b/src/tools/deAsset.ts @@ -39,52 +39,70 @@ export function registerDEAssetTools(server: McpServer, rpc: RPCType) { 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"), - }) + 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"), + }) + .strict() + .refine( + (d) => + [ + d.create_folder, + d.get_all_assets_and_folders, + d.update_asset, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of create_folder, get_all_assets_and_folders, update_asset.", + } + ) ), }, }, diff --git a/src/tools/deComponents.ts b/src/tools/deComponents.ts index 8f99acc..d7cd41c 100644 --- a/src/tools/deComponents.ts +++ b/src/tools/deComponents.ts @@ -25,64 +25,82 @@ export function registerDEComponentsTools(server: McpServer, rpc: RPCType) { 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."), - }) + 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."), + }) + .strict() + .refine( + (d) => + [ + d.check_if_inside_component_view, + d.transform_element_to_component, + d.insert_component_instance, + d.open_component_view, + d.close_component_view, + d.get_all_components, + d.rename_component, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of check_if_inside_component_view, transform_element_to_component, insert_component_instance, open_component_view, close_component_view, get_all_components, rename_component.", + } + ) ), }, }, diff --git a/src/tools/deElement.ts b/src/tools/deElement.ts index 2d253cd..ec223ac 100644 --- a/src/tools/deElement.ts +++ b/src/tools/deElement.ts @@ -124,126 +124,150 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { 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"), - }) + 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"), + }) + .strict() + .refine( + (d) => + [ + d.get_all_elements, + d.get_selected_element, + d.select_element, + d.add_or_update_attribute, + d.remove_attribute, + d.update_id_attribute, + d.set_text, + d.set_style, + d.set_link, + d.set_heading_level, + d.set_image_asset, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of get_all_elements, get_selected_element, select_element, add_or_update_attribute, remove_attribute, update_id_attribute, set_text, set_style, set_link, set_heading_level, set_image_asset.", + } + ) ), }, }, diff --git a/src/tools/dePages.ts b/src/tools/dePages.ts index 7e97607..bcda093 100644 --- a/src/tools/dePages.ts +++ b/src/tools/dePages.ts @@ -25,54 +25,71 @@ export function registerDEPagesTools(server: McpServer, rpc: RPCType) { 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"), + 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"), + }) + .strict() + .refine( + (d) => + [ + d.create_page, + d.create_page_folder, + d.get_current_page, + d.switch_page, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of create_page, create_page_folder, get_current_page, switch_page.", + } + ) ), }, }, diff --git a/src/tools/deStyle.ts b/src/tools/deStyle.ts index cfebb13..8d199bd 100644 --- a/src/tools/deStyle.ts +++ b/src/tools/deStyle.ts @@ -25,128 +25,141 @@ export function registerDEStyleTools(server: McpServer, rpc: RPCType) { 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"), - }) + 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"), + }) + .strict() + .refine( + (d) => + [d.create_style, d.get_styles, d.update_style].filter(Boolean) + .length === 1, + { + message: + "Provide exactly one of create_style, get_styles, update_style.", + } + ) ), }, }, diff --git a/src/tools/deVariable.ts b/src/tools/deVariable.ts index b4c9b9d..f87650a 100644 --- a/src/tools/deVariable.ts +++ b/src/tools/deVariable.ts @@ -25,186 +25,215 @@ export function registerDEVariableTools(server: McpServer, rpc: RPCType) { 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"), - }) + 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"), + }) + .strict() + .refine( + (d) => + [ + d.create_variable_collection, + d.create_variable_mode, + d.get_variable_collections, + d.get_variables, + d.create_color_variable, + d.create_size_variable, + d.create_number_variable, + d.create_percentage_variable, + d.create_font_family_variable, + d.update_color_variable, + d.update_size_variable, + d.update_number_variable, + d.update_percentage_variable, + d.update_font_family_variable, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of create_variable_collection, create_variable_mode, get_variable_collections, get_variables, create_color_variable, create_size_variable, create_number_variable, create_percentage_variable, create_font_family_variable, update_color_variable, update_size_variable, update_number_variable, update_percentage_variable, update_font_family_variable.", + } + ) ), }, }, diff --git a/src/tools/enterprise.ts b/src/tools/enterprise.ts index b0c0c13..366a21a 100644 --- a/src/tools/enterprise.ts +++ b/src/tools/enterprise.ts @@ -185,240 +185,263 @@ export function registerEnterpriseTools( 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."), - }) + 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." + ), + }) + .strict() + .refine( + (d) => + [ + d.list_301_redirects, + d.create_301_redirect, + d.update_301_redirect, + d.delete_301_redirect, + d.get_robots_txt, + d.update_robots_txt, + d.replace_robots_txt, + d.delete_robots_txt, + d.add_well_known_file, + d.remove_well_known_files, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of list_301_redirects, create_301_redirect, update_301_redirect, delete_301_redirect, get_robots_txt, update_robots_txt, replace_robots_txt, delete_robots_txt, add_well_known_file, remove_well_known_files.", + } + ) ) .min(1) .describe("The actions to perform on the enterprise tool."), diff --git a/src/tools/scripts.ts b/src/tools/scripts.ts index c88940f..9e42edb 100644 --- a/src/tools/scripts.ts +++ b/src/tools/scripts.ts @@ -154,97 +154,129 @@ export function registerScriptsTools( "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." - ), - // 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." - ), - }) + 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." + ), + // 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." + ), + }) + .strict() + .refine( + (d) => + [ + d.list_registered_scripts, + d.list_applied_scripts, + d.add_inline_site_script, + d.delete_all_site_scripts, + d.get_page_script, + d.upsert_page_script, + d.delete_all_page_scripts, + ].filter(Boolean).length === 1, + { + message: + "Provide exactly one of list_registered_scripts, list_applied_scripts, add_inline_site_script, delete_all_site_scripts, get_page_script, upsert_page_script, delete_all_page_scripts.", + } + ) ), }, }, diff --git a/src/tools/sites.ts b/src/tools/sites.ts index 1050e4d..c3c5fe9 100644 --- a/src/tools/sites.ts +++ b/src/tools/sites.ts @@ -51,41 +51,58 @@ export function registerSiteTools( "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." - ), - }) + 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." + ), + }) + .strict() + .refine( + (d) => + [d.list_sites, d.get_site, d.publish_site].filter(Boolean) + .length === 1, + { + message: + "Provide exactly one of list_sites, get_site, publish_site.", + } + ) ), }, }, From 3165397d6440dc8903ab3a9258a8c9bc6098a21a Mon Sep 17 00:00:00 2001 From: viratatwebflow Date: Mon, 16 Feb 2026 21:35:07 +0530 Subject: [PATCH 3/3] refactor: update validation logic to allow multiple actions in tools - Modified validation checks in various tools (cms, comments, components, deAsset, deComponents, deElement, dePages, deStyle, deVariable, enterprise, scripts, sites) to require at least one action instead of exactly one. - Updated error messages to reflect the new validation criteria, enhancing user guidance for input requirements. --- src/tools/cms.ts | 4 +- src/tools/comments.ts | 4 +- src/tools/components.ts | 4 +- src/tools/deAsset.ts | 4 +- src/tools/deComponents.ts | 4 +- src/tools/deElement.ts | 4 +- src/tools/dePages.ts | 4 +- src/tools/deStyle.ts | 4 +- src/tools/deVariable.ts | 4 +- src/tools/enterprise.ts | 4 +- src/tools/pages.ts | 232 +++++++++++++++++++++----------------- src/tools/scripts.ts | 4 +- src/tools/sites.ts | 4 +- 13 files changed, 152 insertions(+), 128 deletions(-) diff --git a/src/tools/cms.ts b/src/tools/cms.ts index 6d4f164..b91ad53 100644 --- a/src/tools/cms.ts +++ b/src/tools/cms.ts @@ -478,10 +478,10 @@ export function registerCmsTools( d.update_collection_items, d.publish_collection_items, d.delete_collection_items, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of get_collection_list, get_collection_details, create_collection, create_collection_static_field, create_collection_option_field, create_collection_reference_field, update_collection_field, list_collection_items, create_collection_items, update_collection_items, publish_collection_items, delete_collection_items.", + "Provide at least one of get_collection_list, get_collection_details, create_collection, create_collection_static_field, create_collection_option_field, create_collection_reference_field, update_collection_field, list_collection_items, create_collection_items, update_collection_items, publish_collection_items, delete_collection_items.", } ) ), diff --git a/src/tools/comments.ts b/src/tools/comments.ts index 14d5570..4fd0c1e 100644 --- a/src/tools/comments.ts +++ b/src/tools/comments.ts @@ -260,10 +260,10 @@ export function registerCommentsTools( d.list_comment_threads, d.get_comment_thread, d.list_comment_replies, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of list_comment_threads, get_comment_thread, list_comment_replies.", + "Provide at least one of list_comment_threads, get_comment_thread, list_comment_replies.", } ) ) diff --git a/src/tools/components.ts b/src/tools/components.ts index 8b230f5..3b8b1a5 100644 --- a/src/tools/components.ts +++ b/src/tools/components.ts @@ -260,10 +260,10 @@ export function registerComponentsTools( d.update_component_content, d.get_component_properties, d.update_component_properties, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of list_components, get_component_content, update_component_content, get_component_properties, update_component_properties.", + "Provide at least one of list_components, get_component_content, update_component_content, get_component_properties, update_component_properties.", } ) ), diff --git a/src/tools/deAsset.ts b/src/tools/deAsset.ts index 0014a50..758a2c7 100644 --- a/src/tools/deAsset.ts +++ b/src/tools/deAsset.ts @@ -97,10 +97,10 @@ export function registerDEAssetTools(server: McpServer, rpc: RPCType) { d.create_folder, d.get_all_assets_and_folders, d.update_asset, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of create_folder, get_all_assets_and_folders, update_asset.", + "Provide at least one of create_folder, get_all_assets_and_folders, update_asset.", } ) ), diff --git a/src/tools/deComponents.ts b/src/tools/deComponents.ts index d7cd41c..a185bb3 100644 --- a/src/tools/deComponents.ts +++ b/src/tools/deComponents.ts @@ -95,10 +95,10 @@ export function registerDEComponentsTools(server: McpServer, rpc: RPCType) { d.close_component_view, d.get_all_components, d.rename_component, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of check_if_inside_component_view, transform_element_to_component, insert_component_instance, open_component_view, close_component_view, get_all_components, rename_component.", + "Provide at least one of check_if_inside_component_view, transform_element_to_component, insert_component_instance, open_component_view, close_component_view, get_all_components, rename_component.", } ) ), diff --git a/src/tools/deElement.ts b/src/tools/deElement.ts index ec223ac..ca47e4a 100644 --- a/src/tools/deElement.ts +++ b/src/tools/deElement.ts @@ -262,10 +262,10 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { d.set_link, d.set_heading_level, d.set_image_asset, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of get_all_elements, get_selected_element, select_element, add_or_update_attribute, remove_attribute, update_id_attribute, set_text, set_style, set_link, set_heading_level, set_image_asset.", + "Provide at least one of get_all_elements, get_selected_element, select_element, add_or_update_attribute, remove_attribute, update_id_attribute, set_text, set_style, set_link, set_heading_level, set_image_asset.", } ) ), diff --git a/src/tools/dePages.ts b/src/tools/dePages.ts index bcda093..b90e94e 100644 --- a/src/tools/dePages.ts +++ b/src/tools/dePages.ts @@ -84,10 +84,10 @@ export function registerDEPagesTools(server: McpServer, rpc: RPCType) { d.create_page_folder, d.get_current_page, d.switch_page, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of create_page, create_page_folder, get_current_page, switch_page.", + "Provide at least one of create_page, create_page_folder, get_current_page, switch_page.", } ) ), diff --git a/src/tools/deStyle.ts b/src/tools/deStyle.ts index 8d199bd..8d96836 100644 --- a/src/tools/deStyle.ts +++ b/src/tools/deStyle.ts @@ -154,10 +154,10 @@ export function registerDEStyleTools(server: McpServer, rpc: RPCType) { .refine( (d) => [d.create_style, d.get_styles, d.update_style].filter(Boolean) - .length === 1, + .length >= 1, { message: - "Provide exactly one of create_style, get_styles, update_style.", + "Provide at least one of create_style, get_styles, update_style.", } ) ), diff --git a/src/tools/deVariable.ts b/src/tools/deVariable.ts index f87650a..baa64e5 100644 --- a/src/tools/deVariable.ts +++ b/src/tools/deVariable.ts @@ -228,10 +228,10 @@ export function registerDEVariableTools(server: McpServer, rpc: RPCType) { d.update_number_variable, d.update_percentage_variable, d.update_font_family_variable, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of create_variable_collection, create_variable_mode, get_variable_collections, get_variables, create_color_variable, create_size_variable, create_number_variable, create_percentage_variable, create_font_family_variable, update_color_variable, update_size_variable, update_number_variable, update_percentage_variable, update_font_family_variable.", + "Provide at least one of create_variable_collection, create_variable_mode, get_variable_collections, get_variables, create_color_variable, create_size_variable, create_number_variable, create_percentage_variable, create_font_family_variable, update_color_variable, update_size_variable, update_number_variable, update_percentage_variable, update_font_family_variable.", } ) ), diff --git a/src/tools/enterprise.ts b/src/tools/enterprise.ts index 366a21a..1d5fddb 100644 --- a/src/tools/enterprise.ts +++ b/src/tools/enterprise.ts @@ -436,10 +436,10 @@ export function registerEnterpriseTools( d.delete_robots_txt, d.add_well_known_file, d.remove_well_known_files, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of list_301_redirects, create_301_redirect, update_301_redirect, delete_301_redirect, get_robots_txt, update_robots_txt, replace_robots_txt, delete_robots_txt, add_well_known_file, remove_well_known_files.", + "Provide at least one of list_301_redirects, create_301_redirect, update_301_redirect, delete_301_redirect, get_robots_txt, update_robots_txt, replace_robots_txt, delete_robots_txt, add_well_known_file, remove_well_known_files.", } ) ) diff --git a/src/tools/pages.ts b/src/tools/pages.ts index 171864a..65fd1a5 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -118,110 +118,134 @@ export function registerPagesTools( "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." - ), - }) + 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." + ), + }) + .strict() + .refine( + (d) => + [ + d.list_pages, + d.get_page_metadata, + d.update_page_settings, + d.get_page_content, + d.update_static_content, + ].filter(Boolean).length >= 1, + { + message: + "Provide at least one of list_pages, get_page_metadata, update_page_settings, get_page_content, update_static_content.", + } + ) ), }, }, diff --git a/src/tools/scripts.ts b/src/tools/scripts.ts index 9e42edb..775dc97 100644 --- a/src/tools/scripts.ts +++ b/src/tools/scripts.ts @@ -271,10 +271,10 @@ export function registerScriptsTools( d.get_page_script, d.upsert_page_script, d.delete_all_page_scripts, - ].filter(Boolean).length === 1, + ].filter(Boolean).length >= 1, { message: - "Provide exactly one of list_registered_scripts, list_applied_scripts, add_inline_site_script, delete_all_site_scripts, get_page_script, upsert_page_script, delete_all_page_scripts.", + "Provide at least one of list_registered_scripts, list_applied_scripts, add_inline_site_script, delete_all_site_scripts, get_page_script, upsert_page_script, delete_all_page_scripts.", } ) ), diff --git a/src/tools/sites.ts b/src/tools/sites.ts index c3c5fe9..f96af33 100644 --- a/src/tools/sites.ts +++ b/src/tools/sites.ts @@ -97,10 +97,10 @@ export function registerSiteTools( .refine( (d) => [d.list_sites, d.get_site, d.publish_site].filter(Boolean) - .length === 1, + .length >= 1, { message: - "Provide exactly one of list_sites, get_site, publish_site.", + "Provide at least one of list_sites, get_site, publish_site.", } ) ),