diff --git a/package.json b/package.json index b963c25..14fd409 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "express": "^5.1.0", "socket.io": "^4.8.1", "webflow-api": "3.2.1", - "zod": "^3.24.2" + "zod": "3.25.76" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/src/index.ts b/src/index.ts index c3d43b5..ad89e01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,21 +26,31 @@ function getClient() { return webflowClient; } +// Return the Webflow access token +function getAccessToken() { + if (!process.env.WEBFLOW_TOKEN) { + throw new Error("WEBFLOW_TOKEN is missing"); + } + return process.env.WEBFLOW_TOKEN || ""; +} + // Configure and run local MCP server (stdio transport) async function run() { const server = createMcpServer(); const { callTool } = await initDesignerAppBridge(); registerMiscTools(server); - registerTools(server, getClient); + registerTools(server, getClient, getAccessToken); registerDesignerTools(server, { callTool, getClient, + getAccessToken, }); //Only valid for OSS MCP Version. registerLocalTools(server, { callTool, getClient, + getAccessToken, }); const transport = new StdioServerTransport(); diff --git a/src/mcp.ts b/src/mcp.ts index a80e4ff..3edd3ac 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -17,6 +17,8 @@ import { registerLocalDeMCPConnectionTools, registerCommentsTools, registerEnterpriseTools, + registerWebhookTools, + registerWorkflowsTools, } from "./tools"; import { RPCType } from "./types/RPCType"; @@ -31,7 +33,7 @@ export function createMcpServer() { }, { instructions: `These tools give you access to the Webflow's Data API. If you are ever unsure about anything Webflow API-related, use the "ask_webflow_ai" tool.`, - } + }, ); } @@ -45,7 +47,8 @@ export const requestOptions = { // Register tools export function registerTools( server: McpServer, - getClient: () => WebflowClient + getClient: () => WebflowClient, + getAccessToken: () => string, ) { registerAiChatTools(server); registerCmsTools(server, getClient); @@ -55,6 +58,14 @@ export function registerTools( registerSiteTools(server, getClient); registerCommentsTools(server, getClient); registerEnterpriseTools(server, getClient); + registerWebhookTools(server, getClient); +} + +export function registerWorkflowTools( + server: McpServer, + getAccessToken: () => string +) { + registerWorkflowsTools(server, getAccessToken); } export function registerDesignerTools(server: McpServer, rpc: RPCType) { diff --git a/src/tools/cms.ts b/src/tools/cms.ts index b91ad53..0ff1ab1 100644 --- a/src/tools/cms.ts +++ b/src/tools/cms.ts @@ -34,19 +34,19 @@ import { export function registerCmsTools( server: McpServer, - getClient: () => WebflowClient + getClient: () => WebflowClient, ) { const getCollectionList = async (arg: { siteId: string }) => { const response = await getClient().collections.list( arg.siteId, - requestOptions + requestOptions, ); return response; }; const getCollectionDetails = async (arg: { collection_id: string }) => { const response = await getClient().collections.get( arg.collection_id, - requestOptions + requestOptions, ); return response; }; @@ -58,7 +58,7 @@ export function registerCmsTools( const response = await getClient().collections.create( arg.siteId, arg.request, - requestOptions + requestOptions, ); return response; }; @@ -70,7 +70,7 @@ export function registerCmsTools( const response = await getClient().collections.fields.create( arg.collection_id, arg.request, - requestOptions + requestOptions, ); return response; }; @@ -82,7 +82,7 @@ export function registerCmsTools( const response = await getClient().collections.fields.create( arg.collection_id, arg.request, - requestOptions + requestOptions, ); return response; }; @@ -93,7 +93,7 @@ export function registerCmsTools( const response = await getClient().collections.fields.create( arg.collection_id, arg.request, - requestOptions + requestOptions, ); return response; }; @@ -107,7 +107,7 @@ export function registerCmsTools( arg.collection_id, arg.field_id, arg.request, - requestOptions + requestOptions, ); return response; }; @@ -136,7 +136,7 @@ export function registerCmsTools( const response = await getClient().collections.items.listItems( arg.collection_id, arg.request, - requestOptions + requestOptions, ); return response; }; @@ -157,12 +157,12 @@ export function registerCmsTools( const response = await getClient().collections.items.createItems( arg.collection_id, { - cmsLocaleIds: arg.request.cmsLocaleIds, - isArchived: arg.request.isArchived, - isDraft: arg.request.isDraft, + cmsLocaleIds: arg.request.cmsLocaleIds || [], + isArchived: arg.request.isArchived || false, + isDraft: arg.request.isDraft || false, fieldData: arg.request.fieldData, }, - requestOptions + requestOptions, ); return response; }; @@ -174,7 +174,7 @@ export function registerCmsTools( const response = await getClient().collections.items.updateItems( arg.collection_id, arg.request, - requestOptions + requestOptions, ); return response; }; @@ -189,7 +189,7 @@ export function registerCmsTools( { itemIds: arg.request.itemIds, }, - requestOptions + requestOptions, ); return response; }; @@ -200,7 +200,7 @@ export function registerCmsTools( const response = await getClient().collections.items.deleteItems( arg.collection_id, arg.request, - requestOptions + requestOptions, ); return response; }; @@ -226,7 +226,7 @@ export function registerCmsTools( }) .optional() .describe( - "List all CMS collections in a site. Returns collection metadata including IDs, names, and schemas." + "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 @@ -237,7 +237,7 @@ export function registerCmsTools( }) .optional() .describe( - "Get detailed information about a specific CMS collection including its schema and field definitions." + "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 @@ -247,7 +247,7 @@ export function registerCmsTools( }) .optional() .describe( - "Create a new CMS collection in a site with specified name and schema." + "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 @@ -259,7 +259,7 @@ export function registerCmsTools( }) .optional() .describe( - "Create a new static field in a CMS collection (e.g., text, number, date, etc.)." + "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 @@ -271,7 +271,7 @@ export function registerCmsTools( }) .optional() .describe( - "Create a new option field in a CMS collection with predefined choices." + "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 @@ -283,7 +283,7 @@ export function registerCmsTools( }) .optional() .describe( - "Create a new reference field in a CMS collection that links to items in another collection." + "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 @@ -298,7 +298,7 @@ export function registerCmsTools( }) .optional() .describe( - "Update properties of an existing field in a CMS collection." + "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 @@ -323,19 +323,19 @@ export function registerCmsTools( .string() .optional() .describe( - "Unique identifier for the locale of the CMS Item." + "Unique identifier for the locale of the CMS Item.", ), limit: z .number() .optional() .describe( - "Maximum number of records to be returned (max limit: 100)" + "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." + "Offset used for pagination if the results have more than limit records.", ), name: z .string() @@ -345,7 +345,7 @@ export function registerCmsTools( .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." + "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug.", ), sortBy: WebflowCollectionsItemsListItemsRequestSortBySchema, @@ -357,7 +357,7 @@ export function registerCmsTools( }) .optional() .describe( - "List items in a CMS collection with optional filtering and sorting." + "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 @@ -370,16 +370,18 @@ export function registerCmsTools( cmsLocaleIds: z .array(z.string()) .optional() + .default([]) .describe( - "Unique identifier for the locale of the CMS Item." + "Unique identifier for the locale of the CMS Item.", ), isArchived: z .boolean() .optional() + .default(false) .describe("Indicates if the item is archived."), isDraft: z .boolean() - .optional() + .default(false) .describe("Indicates if the item is a draft."), fieldData: z .array( @@ -389,10 +391,10 @@ export function registerCmsTools( 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." + "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."), }) @@ -408,12 +410,12 @@ export function registerCmsTools( .describe("Unique identifier for the Collection."), request: WebflowCollectionsItemsUpdateItemsRequestSchema.describe( - "Array of items to be updated." + "Array of items to be updated.", ), }) .optional() .describe( - "Update existing items in a CMS collection as drafts." + "Update existing items in a CMS collection as drafts.", ), // POST https://api.webflow.com/v2/collections/:collection_id/items/publish publish_collection_items: z @@ -431,7 +433,7 @@ export function registerCmsTools( }) .optional() .describe( - "Publish existing items in a CMS collection as drafts." + "Publish existing items in a CMS collection as drafts.", ), // DEL https://api.webflow.com/v2/collections/:collection_id/items delete_collection_items: z @@ -449,9 +451,9 @@ export function registerCmsTools( .array(z.string()) .optional() .describe( - "Unique identifier for the locale of the CMS Item." + "Unique identifier for the locale of the CMS Item.", ), - }) + }), ) .describe("Array of items to be deleted."), }) @@ -459,7 +461,7 @@ export function registerCmsTools( }) .optional() .describe( - "Delete existing items in a CMS collection as drafts." + "Delete existing items in a CMS collection as drafts.", ), }) .strict() @@ -482,8 +484,8 @@ export function registerCmsTools( { message: "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.", - } - ) + }, + ), ), }, }, @@ -497,7 +499,7 @@ export function registerCmsTools( } if (action.get_collection_details) { const content = await getCollectionDetails( - action.get_collection_details + action.get_collection_details, ); result.push(textContent(content)); } @@ -507,25 +509,25 @@ export function registerCmsTools( } if (action.create_collection_static_field) { const content = await createCollectionStaticField( - action.create_collection_static_field + action.create_collection_static_field, ); result.push(textContent(content)); } if (action.create_collection_option_field) { const content = await createCollectionOptionField( - action.create_collection_option_field + action.create_collection_option_field, ); result.push(textContent(content)); } if (action.create_collection_reference_field) { const content = await createCollectionReferenceField( - action.create_collection_reference_field + action.create_collection_reference_field, ); result.push(textContent(content)); } if (action.update_collection_field) { const content = await updateCollectionField( - action.update_collection_field + action.update_collection_field, ); result.push(textContent(content)); } @@ -578,6 +580,6 @@ export function registerCmsTools( } catch (error) { return formatErrorResponse(error); } - } + }, ); } diff --git a/src/tools/deComponents.ts b/src/tools/deComponents.ts index a185bb3..e0c3730 100644 --- a/src/tools/deComponents.ts +++ b/src/tools/deComponents.ts @@ -1,17 +1,91 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { RPCType } from "../types/RPCType"; import z from "zod/v3"; -import { DEElementIDSchema, SiteIdSchema } from "../schemas"; -import { formatErrorResponse, formatResponse } from "../utils"; +import { + DEElementIDSchema, + SiteIdSchema, +} from "../schemas"; +import { + formatErrorResponse, + formatResponse, +} from "../utils"; -export function registerDEComponentsTools(server: McpServer, rpc: RPCType) { - const componentsToolRPCCall = async (siteId: string, actions: any) => { +/** + * ComponentSchema - Defines a component to insert with optional nested slot children. + * The `slots.children` field accepts any valid ComponentSchema JSON objects. + * Validation of nested children is performed server-side. + */ +const ComponentSchema = z.object({ + name: z + .string() + .describe("The name of the component to insert."), + slots: z + .array( + z.object({ + name: z.string().describe("The name of the slot."), + children: z + .array(z.any()) + .describe( + "Array of ComponentSchema objects (same shape: { name, slots? }).", + ), + }), + ) + .optional() + .describe("Slots to populate with child components."), +}); + +export function registerDEComponentsTools( + server: McpServer, + rpc: RPCType, +) { + const componentsToolRPCCall = async ( + siteId: string, + actions: any, + ) => { return rpc.callTool("component_tool", { siteId, actions: actions || [], }); }; + const ComponentSchemaValidator: z.ZodType = z.lazy(() => + z.object({ + name: z.string().min(1), + slots: z + .array( + z.object({ + name: z.string().min(1), + children: z.array(ComponentSchemaValidator), + }), + ) + .optional(), + }), + ); + + const componentBuilderRPCCall = async ( + siteId: string, + actions: any, + ) => { + const actionsArray = actions || []; + for (const action of actionsArray) { + if (action.component_schema) { + const result = + ComponentSchemaValidator.safeParse( + action.component_schema, + ); + if (!result.success) { + throw new Error( + `Invalid component_schema in action "${action.build_label}": ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`, + ); + } + } + } + return rpc.callTool("component_builder", { + siteId, + actions: actionsArray, + }); + }; + server.registerTool( "de_component_tool", { @@ -31,58 +105,142 @@ export function registerDEComponentsTools(server: McpServer, rpc: RPCType) { .boolean() .optional() .describe( - "Check if inside component view. this helpful to make changes to the component" + "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"), + name: z + .string() + .describe("The name of the component"), + group: z + .string() + .optional() + .describe("Optional group/category for the component"), + description: z + .string() + .optional() + .describe("Optional description for the component"), }) .optional() - .describe("Transform an element to a component"), + .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"), + .describe( + "The id of the component to insert", + ), creation_position: z - .enum(["append", "prepend"]) + .enum(["append", "prepend", "before", "after"]) .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." + "The position to create component instance on. append/prepend insert as child of the parent element. before/after insert as sibling adjacent to the target element.", ), }) .optional() - .describe("Insert a component on current active page."), + .describe( + "Insert a component on current active page.", + ), open_component_view: z .object({ - component_instance_id: DEElementIDSchema.id, + component_instance_id: + DEElementIDSchema.id, }) .optional() .describe( - "Open a component instance view for changes or reading." + "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." + "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." + "Get all components, only valid if you are connected to Webflow Designer.", + ), + get_component: z + .object({ + component_id: z + .string() + .optional() + .describe("The id of the component to get. Use this or name."), + name: z + .string() + .optional() + .describe("The name of the component. Use this or component_id."), + group: z + .string() + .optional() + .describe("Optional group to narrow the search when using name"), + }) + .optional() + .describe( + "Get a component by ID or by name. Provide component_id for ID lookup, or name (and optional group) for name lookup.", + ), + get_component_metadata: z + .object({ + component_id: z + .string() + .describe("The id of the component"), + }) + .optional() + .describe( + "Get the metadata (name, group, description) of a component", + ), + set_component_metadata: z + .object({ + component_id: z + .string() + .describe("The id of the component"), + name: z + .string() + .optional() + .describe("New name for the component"), + group: z + .string() + .optional() + .describe("New group for the component"), + description: z + .string() + .optional() + .describe("New description for the component"), + }) + .optional() + .describe( + "Update metadata (name, group, description) of a component", ), 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"), + .describe( + "The id of the component to rename", + ), + new_name: z + .string() + .describe("The name of the component"), }) .optional() .describe("Rename a component."), + unregister_component: z + .object({ + component_id: z + .string() + .describe( + "The id of the component to unregister", + ), + }) + .optional() + .describe( + "Unregister a component. DANGEROUS ACTION. USE WITH CAUTION.", + ), }) .strict() .refine( @@ -94,22 +252,102 @@ export function registerDEComponentsTools(server: McpServer, rpc: RPCType) { d.open_component_view, d.close_component_view, d.get_all_components, + d.get_component, + d.get_component_metadata, + d.set_component_metadata, d.rename_component, + d.unregister_component, ].filter(Boolean).length >= 1, { message: - "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.", - } - ) + "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, get_component, get_component_metadata, set_component_metadata, rename_component, unregister_component.", + }, + ), ), }, }, async ({ siteId, actions }) => { try { - return formatResponse(await componentsToolRPCCall(siteId, actions)); + return formatResponse( + await componentsToolRPCCall(siteId, actions), + ); } catch (error) { return formatErrorResponse(error); } - } + }, + ); + + server.registerTool( + "component_builder", + { + annotations: { + openWorldHint: true, + readOnlyHint: false, + }, + description: + "Designer Tool - Component builder to insert component instances on the current active page. Supports inserting into an element (as a child) or into a component instance's slot. Use insert_in_element to add a component inside a container/div/section. Use insert_in_slot to add a component inside a specific slot of an existing component instance.", + inputSchema: { + ...SiteIdSchema, + actions: z.array( + z.object({ + build_label: z + .string() + .describe( + "A label to identify this build action.", + ), + action_type: z + .enum(["insert_in_element", "insert_in_slot"]) + .describe( + "The type of insertion. insert_in_element: insert component as child of parent_element_id. insert_in_slot: insert component into a slot of a component instance identified by parent_element_id.", + ), + parent_element_id: z + .object({ + component: z + .string() + .describe( + "The component id of the element to perform action on.", + ), + element: z + .string() + .describe( + "The element id of the element to perform action on.", + ), + }) + .describe( + "The id of the parent element (for insert_in_element) or the component instance (for insert_in_slot). e.g id:{component:123,element:456}.", + ), + creation_position: z + .enum(["append", "prepend", "before", "after"]) + .describe( + "The position to insert the component. append/prepend insert as child. before/after insert as sibling adjacent to the target element.", + ), + component_schema: ComponentSchema.describe( + "The component schema to insert. Use name to specify which component, and optionally slots to populate child components in the instance's slots.", + ), + slot_name: z + .string() + .optional() + .describe( + "The slot name to insert the component into. Required when action_type is insert_in_slot.", + ), + return_component_info: z + .boolean() + .optional() + .describe( + "Whether to return the component instance info after insertion.", + ), + }), + ), + }, + }, + async ({ actions, siteId }) => { + try { + return formatResponse( + await componentBuilderRPCCall(siteId, actions), + ); + } catch (error) { + return formatErrorResponse(error); + } + }, ); } diff --git a/src/tools/deElement.ts b/src/tools/deElement.ts index ca47e4a..4d28e78 100644 --- a/src/tools/deElement.ts +++ b/src/tools/deElement.ts @@ -1,18 +1,53 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { RPCType } from "../types/RPCType"; import z from "zod/v3"; -import { SiteIdSchema, DEElementIDSchema, DEElementSchema } from "../schemas"; -import { formatErrorResponse, formatResponse } from "../utils"; +import { + SiteIdSchema, + DEElementIDSchema, + DEElementSchema, +} from "../schemas"; +import { + formatErrorResponse, + formatResponse, +} from "../utils"; -export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { - const elementBuilderRPCCall = async (siteId: string, actions: any) => { +export const registerDEElementTools = ( + server: McpServer, + rpc: RPCType, +) => { + const ElementSchemaValidator: z.ZodType = z.lazy(() => + DEElementSchema.extend({ + children: z.array(ElementSchemaValidator).optional(), + }), + ); + + const elementBuilderRPCCall = async ( + siteId: string, + actions: any, + ) => { + const actionsArray = actions || []; + for (const action of actionsArray) { + if (action.element_schema) { + const result = ElementSchemaValidator.safeParse( + action.element_schema, + ); + if (!result.success) { + throw new Error( + `Invalid element_schema: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`, + ); + } + } + } return rpc.callTool("element_builder", { siteId, - actions: actions || [], + actions: actionsArray, }); }; - const elementToolRPCCall = async (siteId: string, actions: any) => { + const elementToolRPCCall = async ( + siteId: string, + actions: any, + ) => { return rpc.callTool("element_tool", { siteId, actions: actions || [], @@ -21,7 +56,7 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { const elementSnapshotToolRPCCall = async ( siteId: string, - action: any + action: any, ): Promise< | { status: string; @@ -48,67 +83,67 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { readOnlyHint: false, }, description: - "Designer Tool - Element builder to create element on current active page. only create elements upto max 3 levels deep. divide your elements into smaller elements to create complex structures. recall this tool to create more elements. but max level is upto 3 levels. you can have as many children as you want. but max level is 3 levels.", + "Designer Tool - Element builder to create element on current active page.", inputSchema: { ...SiteIdSchema, actions: z.array( z.object({ + build_label: z + .string() + .optional() + .describe( + "A label to identify this build action in the results.", + ), parent_element_id: z .object({ component: z .string() .describe( - "The component id of the element to perform action on." + "The component id of the element to perform action on.", ), element: z .string() .describe( - "The element id of the element to perform action on." + "The element id of the element to perform action on.", ), }) .describe( - "The id of the parent element to create element on, you can find it from id field on element. e.g id:{component:123,element:456}." + "The id of the parent element to create element on, you can find it from id field on element. e.g id:{component:123,element:456}.", ), creation_position: z - .enum(["append", "prepend"]) + .enum(["append", "prepend", "before", "after"]) .describe( - "The position to create element on. append to the end of the parent element or prepend to the beginning of the parent element. as child of the parent element." + "The position to create element on. append/prepend insert as child of the parent element. before/after insert as sibling adjacent to the target element.", ), element_schema: DEElementSchema.extend({ children: z - .array( - DEElementSchema.extend({ - children: z - .array( - DEElementSchema.extend({ - children: z - .array( - DEElementSchema.extend({ - children: z.array(DEElementSchema).optional(), - }) - ) - .optional(), - }) - ) - .optional(), - }) - ) + .array(z.any()) .optional() .describe( - "The children of the element. only valid for container, section, div block, valid DOM elements." + "Array of ElementSchema objects (same shape as element_schema with optional children)..", ), - }).describe("element schema of element to create."), - }) + }).describe( + "ElementSchema - element schema of element to create. Children are recursive ElementSchema objects.", + ), + return_element_info: z + .boolean() + .optional() + .describe( + "Whether to return full element info for the created element. Defaults to false.", + ), + }), ), }, }, async ({ actions, siteId }) => { try { - return formatResponse(await elementBuilderRPCCall(siteId, actions)); + return formatResponse( + await elementBuilderRPCCall(siteId, actions), + ); } catch (error) { return formatErrorResponse(error); } - } + }, ); server.registerTool( @@ -127,29 +162,41 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { 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"), - }) + .boolean() .optional() - .describe("Get all elements on the current active page"), + .describe( + "Get all elements on the current active page", + ), + get_selected_element: z - .boolean() + .object({ + children_depth: z + .number() + .min(-1) + .describe( + "The depth of children to include. 0 for no children. -1 for all children. X for X levels deep.", + ), + }) .optional() - .describe("Get selected element on the current active page"), + .describe( + "Get selected element on the current active page", + ), select_element: z .object({ ...DEElementIDSchema, }) .optional() - .describe("Select an element on the current active page"), + .describe( + "Select an element on the current active page", + ), + remove_element: z + .object({ + ...DEElementIDSchema, + }) + .optional() + .describe( + "Remove an element from the current active page. DANGEROUS ACTION. USE WITH CAUTION.", + ), add_or_update_attribute: z .object({ ...DEElementIDSchema, @@ -159,43 +206,57 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { name: z .string() .describe( - "The name of the attribute to add or update." + "The name of the attribute to add or update.", ), value: z .string() .describe( - "The value of the attribute to add or update." + "The value of the attribute to add or update.", ), - }) + }), ) - .describe("The attributes to add or update."), + .describe( + "The attributes to add or update.", + ), }) .optional() - .describe("Add or update an attribute on the element"), + .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."), + .describe( + "The names of the attributes to remove.", + ), }) .optional() - .describe("Remove an attribute from the element"), + .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." + "The new #id of the element to update the id attribute to.", ), }) .optional() - .describe("Update the #id attribute of the element"), + .describe( + "Update the #id attribute of the element", + ), set_text: z .object({ ...DEElementIDSchema, - text: z.string().describe("The text to set on the element."), + text: z + .string() + .describe( + "The text to set on the element.", + ), }) .optional() .describe("Set text on the element"), @@ -204,22 +265,33 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { ...DEElementIDSchema, style_names: z .array(z.string()) - .describe("The style names to set on the element."), + .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 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."), + .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." + "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() @@ -232,20 +304,143 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { .min(1) .max(6) .describe( - "The heading level to set on the element. 1 to 6." + "The heading level to set on the element. 1 to 6.", ), }) .optional() - .describe("Set heading level on the heading element."), + .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."), + .describe( + "The image asset id to set on the element.", + ), + }) + .optional() + .describe( + "Set image asset on the image element", + ), + query_elements: z + .object({ + queries: z.array( + z.object({ + label: z + .string() + .optional() + .describe( + "A label to identify this query in the results.", + ), + element_id: z + .object({ + component: z.string(), + element: z.string(), + }) + .optional() + .describe( + "Filter by element ID. Exact match. Bypasses other filters — returns the element directly.", + ), + element_filter: z + .object({ + type: z + .string() + .optional() + .describe( + 'Filter by element type. Exact match. e.g. "Heading", "Image", "Link", "Block", "DOM", "FormForm".', + ), + text: z + .string() + .optional() + .describe( + "Filter by text content. Case-insensitive substring match.", + ), + style: z + .string() + .optional() + .describe( + "Filter by style/class name. Case-insensitive substring match against any applied style name.", + ), + tag: z + .string() + .optional() + .describe( + 'Filter by HTML tag. For Block/DOM elements. Case-insensitive exact match. e.g. "section", "nav", "div".', + ), + attribute_name: z + .string() + .optional() + .describe( + "Filter by attribute name. Case-insensitive exact match on attribute key.", + ), + attribute_value: z + .string() + .optional() + .describe( + "Filter by attribute value. Used with attribute_name. Case-insensitive substring match.", + ), + }) + .optional() + .describe( + "Filter by element properties. Cannot be combined with component_filter.", + ), + component_filter: z + .object({ + component_name: z + .string() + .optional() + .describe( + "Filter by component name. Case-insensitive substring match.", + ), + slot_name: z + .string() + .optional() + .describe( + "Filter by slot name. Case-insensitive substring match.", + ), + }) + .optional() + .describe( + "Filter by component properties. Cannot be combined with element_filter.", + ), + scope_element_id: z + .object({ + component: z.string(), + element: z.string(), + }) + .optional() + .describe( + "Scope search to descendants of this element (element itself excluded).", + ), + return_parent: z + .enum(["parent", "ancestor"]) + .optional() + .describe( + 'Instead of returning matched elements, return their parent. "parent" = immediate parent. "ancestor" = any ancestor whose subtree contains matches.', + ), + children_depth: z + .number() + .optional() + .describe( + "Include N levels of children in each result. 0 = no children (default), -1 = all.", + ), + limit: z + .number() + .min(1) + .max(200) + .optional() + .describe( + "Max results for this query. Default: 50, Max: 200.", + ), + }), + ), }) .optional() - .describe("Set image asset on the image element"), + .describe( + "Query elements on the current active page. Supports filtering by type, text, style, tag, attributes, component name, slot name. Supports scoped search, parent lookup, and multiple queries per call.", + ), }) .strict() .refine( @@ -262,22 +457,114 @@ export const registerDEElementTools = (server: McpServer, rpc: RPCType) => { d.set_link, d.set_heading_level, d.set_image_asset, + d.remove_element, + d.query_elements, ].filter(Boolean).length >= 1, { message: - "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.", - } - ) + "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, query_elements.", + }, + ), ), }, }, async ({ actions, siteId }) => { try { - return formatResponse(await elementToolRPCCall(siteId, actions)); + return formatResponse( + await elementToolRPCCall(siteId, actions), + ); } catch (error) { return formatErrorResponse(error); } - } + }, + ); + + const whtmlBuilderRPCCall = async ( + siteId: string, + actions: any, + ) => { + return rpc.callTool("whtml_builder", { + siteId, + actions: actions || [], + }); + }; + + server.registerTool( + "whtml_builder", + { + annotations: { + openWorldHint: true, + readOnlyHint: false, + }, + description: + "Designer Tool - WHTML builder to insert elements from HTML and CSS strings on the current active page. Accepts HTML markup and optional CSS rules, constructs WHTML, and inserts into a parent element.", + inputSchema: { + ...SiteIdSchema, + actions: z.array( + z.object({ + build_label: z + .string() + .describe( + "A label to identify this build action in the results.", + ), + parent_element_id: z + .object({ + component: z + .string() + .describe( + "The component id of the element to perform action on.", + ), + element: z + .string() + .describe( + "The element id of the element to perform action on.", + ), + }) + .describe( + "The id of the parent element to insert WHTML into. e.g id:{component:123,element:456}.", + ), + creation_position: z + .enum(["append", "prepend", "before", "after"]) + .describe( + "The position to insert the element. append/prepend insert as child of the parent element. before/after insert as sibling adjacent to the target element.", + ), + html: z + .string() + .min(1) + .describe( + "HTML markup string to insert. Must not contain