# Directory Structure
```
├── .gitignore
├── Dockerfile
├── LICENSE
├── package.json
├── README.md
├── smithery.yaml
├── src
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 | package-lock.json
4 | yarn.lock
5 |
6 | # Build outputs
7 | dist/
8 | build/
9 | *.tsbuildinfo
10 |
11 | # Environment variables
12 | .env
13 | .env.local
14 | .env.*.local
15 |
16 | # Logs
17 | logs/
18 | *.log
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | # IDE specific files
24 | .vscode/
25 |
26 | # Temporary files
27 | *.tmp
28 | *.temp
29 | .cache/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Webflow MCP Server
2 | [](https://smithery.ai/server/@kapilduraphe/webflow-mcp-server)
3 |
4 | This MCP server enables Claude to interact with Webflow's APIs.
5 |
6 | <a href="https://glama.ai/mcp/servers/un9r0vtmku">
7 | <img width="380" height="200" src="https://glama.ai/mcp/servers/un9r0vtmku/badge" alt="Webflow Server MCP server" />
8 | </a>
9 |
10 | ## Prerequisites
11 |
12 | - Node.js (v16 or higher)
13 | - Claude Desktop App
14 | - Webflow Account
15 | - Webflow API Token (Site token or OAuth Acces Token)
16 |
17 | ## Setup Instructions
18 |
19 | ### 1. Create a Webflow API Token
20 |
21 | - Log in to your Webflow account
22 | - Navigate to Site Settings > Apps & Integrations
23 | - Generate a new API token
24 | - Copy the token value (you won't be able to see it again)
25 |
26 | Alternatively, you can also generate an OAuth Access Token.
27 |
28 | ### 2. Initial Project Setup
29 |
30 | Install dependencies:
31 |
32 | ```bash
33 | npm install
34 | ```
35 |
36 | ### 3. Configure Environment Variables
37 |
38 | Create a `.env` file for local development (don't commit this file):
39 |
40 | ```plaintext
41 | WEBFLOW_API_TOKEN=your-api-token
42 | ```
43 |
44 | ### 4. Configure Claude Desktop
45 |
46 | Open your Claude Desktop configuration file:
47 |
48 | For MacOS:
49 |
50 | ```bash
51 | code ~/Library/Application\ Support/Claude/claude_desktop_config.json
52 | ```
53 |
54 | For Windows:
55 |
56 | ```bash
57 | code %AppData%\Claude\claude_desktop_config.json
58 | ```
59 |
60 | Add or update the configuration:
61 |
62 | ```json
63 | {
64 | "mcpServers": {
65 | "webflow": {
66 | "command": "node",
67 | "args": [
68 | "/ABSOLUTE/PATH/TO/YOUR/build/index.js"
69 | ],
70 | "env": {
71 | "WEBFLOW_API_TOKEN": "your-api-token"
72 | }
73 | }
74 | }
75 | }
76 | ```
77 |
78 | Save the file and restart Claude Desktop.
79 |
80 | ### Installing via Smithery
81 |
82 | To install Webflow MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@kapilduraphe/webflow-mcp-server):
83 |
84 | ```bash
85 | npx -y @smithery/cli install @kapilduraphe/webflow-mcp-server --client claude
86 | ```
87 |
88 | ## Available Tools
89 |
90 | The server currently provides the following tools:
91 |
92 | ### get_sites
93 |
94 | Retrieves a list of all Webflow sites accessible to the authenticated user. Returns detailed information including:
95 |
96 | - Site Display Name and Short Name
97 | - Site ID and Workspace ID
98 | - Creation, Last Updated, and Last Published Dates
99 | - Preview URL
100 | - Time Zone settings
101 | - Custom Domains configuration
102 | - Localization settings (primary and secondary locales)
103 | - Data collection preferences
104 |
105 | ### get_site
106 |
107 | Retrieves detailed information about a specific Webflow site by ID. Requires a siteId parameter and returns the same detailed information as get_sites for a single site.
108 |
109 | ## Type Definitions
110 |
111 | ```typescript
112 | interface WebflowApiError {
113 | status?: number;
114 | message: string;
115 | code?: string;
116 | }
117 |
118 | interface WebflowCustomDomain {
119 | id: string;
120 | url: string;
121 | lastPublished: string;
122 | }
123 |
124 | interface WebflowLocale {
125 | id: string;
126 | cmsLocaleId: string;
127 | enabled: boolean;
128 | displayName: string;
129 | redirect: boolean;
130 | subdirectory: string;
131 | tag: string;
132 | }
133 |
134 | interface WebflowSite {
135 | id: string;
136 | workspaceId: string;
137 | createdOn: string;
138 | displayName: string;
139 | shortName: string;
140 | lastPublished: string;
141 | lastUpdated: string;
142 | previewUrl: string;
143 | timeZone: string;
144 | parentFolderId?: string;
145 | customDomains: WebflowCustomDomain[];
146 | locales: {
147 | primary: WebflowLocale;
148 | secondary: WebflowLocale[];
149 | };
150 | dataCollectionEnabled: boolean;
151 | dataCollectionType: string;
152 | }
153 | ```
154 |
155 | ## Error Handling
156 |
157 | The server handles various error scenarios:
158 |
159 | ### Environment Errors
160 |
161 | - Missing WEBFLOW_API_TOKEN
162 | - Invalid API token
163 |
164 | ## Troubleshooting
165 |
166 | ### Common Issues
167 |
168 | #### Tools not appearing in Claude
169 |
170 | - Check Claude Desktop logs
171 | - Verify WEBFLOW_API_TOKEN is set correctly
172 | - Ensure the path to index.js is absolute and correct
173 |
174 | #### Authentication Errors
175 |
176 | - Verify your API token is valid
177 | - Check if the token has the necessary permissions
178 | - Ensure the token hasn't expired
179 |
180 | ### Viewing Logs
181 |
182 | To view server logs:
183 |
184 | For MacOS/Linux:
185 |
186 | ```bash
187 | tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
188 | ```
189 |
190 | For Windows:
191 |
192 | ```powershell
193 | Get-Content -Path "$env:AppData\Claude\Logs\mcp*.log" -Wait -Tail 20
194 | ```
195 |
196 | ### Environment Variables
197 |
198 | If you're getting environment variable errors, verify:
199 |
200 | - `WEBFLOW_API_TOKEN`: Should be a valid API token
201 |
202 | ## Security Considerations
203 |
204 | - Keep your API token secure
205 | - Don't commit credentials to version control
206 | - Use environment variables for sensitive data
207 | - Regularly rotate API tokens
208 | - Monitor API usage in Webflow
209 | - Use minimum required permissions for API token
210 |
211 | ## Support
212 |
213 | If you encounter any issues:
214 |
215 | - Check the troubleshooting section above
216 | - Review Claude Desktop logs
217 | - Examine the server's error output
218 | - Check Webflow's API documentation
219 |
220 | ## License
221 |
222 | MIT License - See LICENSE file for details.
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "allowJs": true,
7 | "outDir": "./dist",
8 | "rootDir": "./src",
9 | "strict": true,
10 | "esModuleInterop": true,
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true
13 | },
14 | "include": ["src/**/*"],
15 | "exclude": ["node_modules"]
16 | }
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - webflowApiToken
10 | properties:
11 | webflowApiToken:
12 | type: string
13 | description: The API token for accessing Webflow's APIs.
14 | commandFunction:
15 | # A function that produces the CLI command to start the MCP on stdio.
16 | |-
17 | (config) => ({command:'node',args:['dist/index.js'],env:{WEBFLOW_API_TOKEN:config.webflowApiToken}})
18 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "webflow-mcp-server",
3 | "version": "1.0.0",
4 | "description": "An MCP server for using Webflow APIs",
5 | "type": "module",
6 | "license": "MIT",
7 | "bin": {
8 | "mcp-server-okta": "dist/index.js"
9 | },
10 | "files": [
11 | "dist"
12 | ],
13 | "scripts": {
14 | "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
15 | "prepare": "npm run build",
16 | "watch": "tsc --watch"
17 | },
18 |
19 | "dependencies": {
20 | "@modelcontextprotocol/sdk": "^1.0.4",
21 | "webflow-api": "^3.0.0",
22 | "dotenv": "^16.4.1",
23 | "zod": "^3.22.4"
24 | },
25 | "devDependencies": {
26 | "@types/node": "^20.11.5",
27 | "prettier": "^3.2.4",
28 | "ts-node": "^10.9.2",
29 | "typescript": "^5.3.3"
30 | }
31 | }
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | # Stage 1: Build
3 | FROM node:16-alpine AS builder
4 |
5 | # Set working directory
6 | WORKDIR /app
7 |
8 | # Copy package.json and package-lock.json
9 | COPY package.json ./
10 |
11 | # Install dependencies
12 | RUN npm install --ignore-scripts
13 |
14 | # Copy the source code
15 | COPY . .
16 |
17 | # Build the project
18 | RUN npm run build
19 |
20 | # Stage 2: Run
21 | FROM node:16-alpine
22 |
23 | # Set working directory
24 | WORKDIR /app
25 |
26 | # Copy only the necessary files
27 | COPY --from=builder /app/dist /app/dist
28 | COPY package.json ./
29 |
30 | # Set environment variables (you should set this in your environment or secrets)
31 | ENV WEBFLOW_API_TOKEN=your-api-token
32 |
33 | # Install production dependencies
34 | RUN npm install --production
35 |
36 | # Start the server
37 | ENTRYPOINT ["node", "dist/index.js"]
38 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3 | import {
4 | CallToolRequestSchema,
5 | ListToolsRequestSchema,
6 | } from "@modelcontextprotocol/sdk/types.js";
7 |
8 | import { WebflowClient } from "webflow-api";
9 | import { z } from "zod";
10 |
11 | const accessToken =
12 | process.env.WEBFLOW_API_TOKEN ||
13 | (() => {
14 | throw new Error("WEBFLOW_API_TOKEN is not defined");
15 | })();
16 |
17 | // Initialize the server
18 | const server = new Server(
19 | {
20 | name: "webflow-mcp-server",
21 | version: "1.0.0",
22 | },
23 | {
24 | capabilities: {
25 | tools: {},
26 | },
27 | }
28 | );
29 |
30 | const schemas = {
31 | toolInputs: {
32 | getSite: z.object({
33 | siteId: z.string().min(1, "Site ID is required"),
34 | }),
35 | getSites: z.object({}),
36 | },
37 | };
38 |
39 | interface WebflowApiError {
40 | status?: number;
41 | message: string;
42 | code?: string;
43 | }
44 |
45 | type ToolHandler = (args: unknown) => Promise<{
46 | content: Array<{ type: "text"; text: string }>;
47 | }>;
48 |
49 | // Utility functions
50 | function isWebflowApiError(error: unknown): error is WebflowApiError {
51 | return error !== null && typeof error === "object" && "code" in error;
52 | }
53 |
54 | function formatDate(date: Date | undefined | null): string {
55 | if (!date) return "N/A";
56 | return date.toLocaleString();
57 | }
58 |
59 | // Tool definitions
60 | const TOOL_DEFINITIONS = [
61 | {
62 | name: "get_site",
63 | description:
64 | "Retrieve detailed information about a specific Webflow site by ID, including workspace, creation date, display name, and publishing details",
65 | inputSchema: {
66 | type: "object",
67 | properties: {
68 | siteId: {
69 | type: "string",
70 | description: "The unique identifier of the Webflow site",
71 | },
72 | },
73 | required: ["siteId"],
74 | },
75 | },
76 | {
77 | name: "get_sites",
78 | description:
79 | "Retrieve a list of all Webflow sites accessible to the authenticated user",
80 | inputSchema: {
81 | type: "object",
82 | properties: {},
83 | required: [],
84 | },
85 | },
86 | ];
87 |
88 | // Tool handlers
89 | const toolHandlers: Record<string, ToolHandler> = {
90 | get_site: async (args: unknown) => {
91 | const { siteId } = schemas.toolInputs.getSite.parse(args);
92 |
93 | try {
94 | const webflow = new WebflowClient({ accessToken });
95 | const site = await webflow.sites.get(siteId);
96 |
97 | if (!site) {
98 | throw new Error("Site not found");
99 | }
100 |
101 | const formattedSite = `• Site Details:
102 | ID: ${site.id}
103 | Display Name: ${site.displayName}
104 | Short Name: ${site.shortName}
105 |
106 | - Workspace Information:
107 | Workspace ID: ${site.workspaceId}
108 |
109 | - Dates:
110 | Created On: ${formatDate(site?.createdOn)}
111 | Last Published: ${formatDate(site?.lastPublished)}
112 |
113 | - URLs:
114 | Preview URL: ${site.previewUrl || "N/A"}`;
115 |
116 | return {
117 | content: [
118 | {
119 | type: "text" as const,
120 | text: formattedSite,
121 | },
122 | ],
123 | };
124 | } catch (error: unknown) {
125 | if (isWebflowApiError(error) && error.code === "NOT_FOUND") {
126 | return {
127 | content: [
128 | {
129 | type: "text" as const,
130 | text: `Site with ID ${siteId} not found.`,
131 | },
132 | ],
133 | };
134 | }
135 | console.error("Error fetching site:", error);
136 | throw new Error("Failed to fetch site details");
137 | }
138 | },
139 |
140 | get_sites: async () => {
141 | try {
142 | const webflow = new WebflowClient({ accessToken });
143 | const { sites } = await webflow.sites.list();
144 |
145 | if (!Array.isArray(sites) || sites.length === 0) {
146 | return {
147 | content: [
148 | {
149 | type: "text" as const,
150 | text: "No sites found for this account.",
151 | },
152 | ],
153 | };
154 | }
155 |
156 | const formattedSites = sites
157 | .map(
158 | (site) => `
159 | • Site: ${site.displayName}
160 | - ID: ${site.id}
161 | - Workspace: ${site.workspaceId}
162 | - Created: ${formatDate(site?.createdOn)}
163 | - Last Published: ${formatDate(site?.lastPublished)}
164 | - Preview URL: ${site.previewUrl || "N/A"}
165 | `
166 | )
167 | .join("\n");
168 |
169 | return {
170 | content: [
171 | {
172 | type: "text" as const,
173 | text: `Found ${sites.length} sites:\n${formattedSites}`,
174 | },
175 | ],
176 | };
177 | } catch (error: unknown) {
178 | console.error("Error fetching sites:", error);
179 | throw new Error("Failed to fetch sites list");
180 | }
181 | },
182 | };
183 |
184 | // Register tool handlers
185 | server.setRequestHandler(ListToolsRequestSchema, async () => {
186 | console.error("Tools requested by client");
187 | return { tools: TOOL_DEFINITIONS };
188 | });
189 |
190 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
191 | const { name, arguments: args } = request.params;
192 |
193 | try {
194 | const handler = toolHandlers[name as keyof typeof toolHandlers];
195 | if (!handler) {
196 | throw new Error(`Unknown tool: ${name}`);
197 | }
198 |
199 | return await handler(args);
200 | } catch (error) {
201 | console.error(`Error executing tool ${name}:`, error);
202 | throw error;
203 | }
204 | });
205 |
206 | // Start the server
207 | async function main() {
208 | try {
209 | // Check for required environment variables
210 | const requiredEnvVars = ["WEBFLOW_API_TOKEN"];
211 |
212 | const missingVars = requiredEnvVars.filter(
213 | (varName) => !process.env[varName]
214 | );
215 | if (missingVars.length > 0) {
216 | console.error(
217 | `Missing required environment variables: ${missingVars.join(", ")}`
218 | );
219 | process.exit(1);
220 | }
221 |
222 | console.error("Starting server with env vars:", {
223 | WEBFLOW_API_TOKEN: "[REDACTED]",
224 | });
225 |
226 | const transport = new StdioServerTransport();
227 | console.error("Created transport");
228 |
229 | await server.connect(transport);
230 | console.error("Connected to transport");
231 |
232 | console.error("Webflow MCP Server running on stdio");
233 | } catch (error) {
234 | console.error("Startup error:", error);
235 | process.exit(1);
236 | }
237 | }
238 |
239 | main().catch((error) => {
240 | console.error("Fatal error in main():", error);
241 | process.exit(1);
242 | });
243 |
```