@pipeworx/mcp-webflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/package.json +20 -0
- package/server.json +18 -0
- package/src/index.ts +285 -0
- package/tsconfig.json +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pipeworx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# mcp-webflow
|
|
2
|
+
|
|
3
|
+
Webflow MCP Pack
|
|
4
|
+
|
|
5
|
+
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 728+ live data sources.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
| Tool | Description |
|
|
10
|
+
|------|-------------|
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
Add to your MCP client (Claude Desktop, Cursor, Windsurf, etc.):
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"webflow": {
|
|
20
|
+
"url": "https://gateway.pipeworx.io/webflow/mcp"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or connect to the full Pipeworx gateway for access to all 728+ data sources:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"pipeworx": {
|
|
32
|
+
"url": "https://gateway.pipeworx.io/mcp"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Using with ask_pipeworx
|
|
39
|
+
|
|
40
|
+
Instead of calling tools directly, you can ask questions in plain English:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
ask_pipeworx({ question: "your question about Webflow data" })
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The gateway picks the right tool and fills the arguments automatically.
|
|
47
|
+
|
|
48
|
+
## More
|
|
49
|
+
|
|
50
|
+
- [All tools and guides](https://github.com/pipeworx-io/examples)
|
|
51
|
+
- [pipeworx.io](https://pipeworx.io)
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pipeworx/mcp-webflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Webflow MCP Pack",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"keywords": ["mcp", "mcp-server", "model-context-protocol", "pipeworx", "webflow"],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/pipeworx-io/mcp-webflow"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.7.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.pipeworx-io/webflow",
|
|
4
|
+
"title": "Webflow",
|
|
5
|
+
"description": "Webflow MCP Pack",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"websiteUrl": "https://pipeworx.io/packs/webflow",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "https://github.com/pipeworx-io/mcp-webflow",
|
|
10
|
+
"source": "github"
|
|
11
|
+
},
|
|
12
|
+
"remotes": [
|
|
13
|
+
{
|
|
14
|
+
"type": "streamable-http",
|
|
15
|
+
"url": "https://gateway.pipeworx.io/webflow/mcp"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
interface McpToolDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: 'object';
|
|
6
|
+
properties: Record<string, unknown>;
|
|
7
|
+
required?: string[];
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface McpToolExport {
|
|
12
|
+
tools: McpToolDefinition[];
|
|
13
|
+
callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
14
|
+
meter?: { credits: number };
|
|
15
|
+
cost?: Record<string, unknown>;
|
|
16
|
+
provider?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Webflow MCP Pack
|
|
21
|
+
*
|
|
22
|
+
* Requires OAuth connection — gateway injects credentials via _context.webflow.
|
|
23
|
+
* Read-only access to Webflow sites and CMS content via the Webflow Data API v2.
|
|
24
|
+
* Tools: list sites, get site, list collections, list collection items, get collection item.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
interface WebflowContext {
|
|
29
|
+
webflow?: { accessToken: string };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const API = 'https://api.webflow.com/v2';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fetch helper for the Webflow Data API v2.
|
|
36
|
+
* - Returns { error: 'connection_required' } when no OAuth token is present.
|
|
37
|
+
* - Returns { error: <status>, message: <body text> } on non-2xx responses.
|
|
38
|
+
* - Otherwise returns the parsed JSON body.
|
|
39
|
+
*/
|
|
40
|
+
async function wFetch(
|
|
41
|
+
ctx: WebflowContext,
|
|
42
|
+
url: string,
|
|
43
|
+
options: RequestInit = {},
|
|
44
|
+
): Promise<unknown> {
|
|
45
|
+
if (!ctx.webflow) {
|
|
46
|
+
return {
|
|
47
|
+
error: 'connection_required',
|
|
48
|
+
message: 'Connect your Webflow account at https://pipeworx.io/account',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const res = await fetch(url, {
|
|
52
|
+
...options,
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${ctx.webflow.accessToken}`,
|
|
55
|
+
accept: 'application/json',
|
|
56
|
+
...(options.headers ?? {}),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
return { error: res.status, message: text };
|
|
62
|
+
}
|
|
63
|
+
return res.json();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tools: McpToolExport['tools'] = [
|
|
67
|
+
{
|
|
68
|
+
name: 'list_sites',
|
|
69
|
+
description:
|
|
70
|
+
'List all Webflow sites accessible to the connected account. Returns each site\'s id, display name, short name, preview URL, last published time, and custom domains. Use to discover which Webflow sites are available before reading their CMS collections or site content.',
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: 'object' as const,
|
|
73
|
+
properties: {},
|
|
74
|
+
required: [],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'get_site',
|
|
79
|
+
description:
|
|
80
|
+
'Get metadata for a single Webflow site by its id, including display name, short name, preview URL, time zone, last published time, and custom domains. Use after list_sites to inspect a specific site before browsing its CMS collections and content.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object' as const,
|
|
83
|
+
properties: {
|
|
84
|
+
site_id: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'The id of the Webflow site to retrieve (from list_sites).',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ['site_id'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'list_collections',
|
|
94
|
+
description:
|
|
95
|
+
'List the CMS collections defined on a Webflow site. Returns each collection\'s id, display name, slug, singular name, created time, and last updated time. Use to discover the CMS structure of a site before listing or reading collection items (the actual site content).',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object' as const,
|
|
98
|
+
properties: {
|
|
99
|
+
site_id: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'The id of the Webflow site whose CMS collections to list (from list_sites).',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: ['site_id'],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'list_collection_items',
|
|
109
|
+
description:
|
|
110
|
+
'List the items (records) in a Webflow CMS collection — the live site content such as blog posts, products, or team members. Returns paginated items with their id, draft/archived status, last published time, and fieldData (the CMS field values). Use to read the actual content stored in a Webflow collection.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object' as const,
|
|
113
|
+
properties: {
|
|
114
|
+
collection_id: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
description: 'The id of the Webflow CMS collection whose items to list (from list_collections).',
|
|
117
|
+
},
|
|
118
|
+
limit: {
|
|
119
|
+
type: 'number',
|
|
120
|
+
description: 'Maximum number of items to return (default 50, max 100).',
|
|
121
|
+
},
|
|
122
|
+
offset: {
|
|
123
|
+
type: 'number',
|
|
124
|
+
description: 'Number of items to skip for pagination (default 0).',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ['collection_id'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'get_collection_item',
|
|
132
|
+
description:
|
|
133
|
+
'Get a single item (record) from a Webflow CMS collection by its id. Returns the item\'s id, draft/archived status, last published time, and fieldData (the full CMS field values). Use after list_collection_items to read one piece of Webflow site content in detail.',
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: 'object' as const,
|
|
136
|
+
properties: {
|
|
137
|
+
collection_id: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'The id of the Webflow CMS collection containing the item (from list_collections).',
|
|
140
|
+
},
|
|
141
|
+
item_id: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'The id of the CMS item to retrieve (from list_collection_items).',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
required: ['collection_id', 'item_id'],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
interface WebflowSite {
|
|
152
|
+
id?: string;
|
|
153
|
+
displayName?: string;
|
|
154
|
+
shortName?: string;
|
|
155
|
+
previewUrl?: string;
|
|
156
|
+
timeZone?: string;
|
|
157
|
+
lastPublished?: string;
|
|
158
|
+
customDomains?: unknown;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface WebflowCollection {
|
|
162
|
+
id?: string;
|
|
163
|
+
displayName?: string;
|
|
164
|
+
slug?: string;
|
|
165
|
+
singularName?: string;
|
|
166
|
+
createdOn?: string;
|
|
167
|
+
lastUpdated?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface WebflowItem {
|
|
171
|
+
id?: string;
|
|
172
|
+
isDraft?: boolean;
|
|
173
|
+
isArchived?: boolean;
|
|
174
|
+
lastPublished?: string;
|
|
175
|
+
fieldData?: unknown;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
|
|
179
|
+
const context = (args._context ?? {}) as WebflowContext;
|
|
180
|
+
delete args._context;
|
|
181
|
+
|
|
182
|
+
switch (name) {
|
|
183
|
+
case 'list_sites': {
|
|
184
|
+
const result = await wFetch(context, `${API}/sites`);
|
|
185
|
+
const sites = (result as { sites?: WebflowSite[] }).sites;
|
|
186
|
+
if (Array.isArray(sites)) {
|
|
187
|
+
return sites.map((s) => ({
|
|
188
|
+
id: s.id,
|
|
189
|
+
displayName: s.displayName,
|
|
190
|
+
shortName: s.shortName,
|
|
191
|
+
previewUrl: s.previewUrl,
|
|
192
|
+
lastPublished: s.lastPublished,
|
|
193
|
+
customDomains: s.customDomains,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
case 'get_site': {
|
|
199
|
+
const siteId = args.site_id as string;
|
|
200
|
+
const result = await wFetch(context, `${API}/sites/${encodeURIComponent(siteId)}`);
|
|
201
|
+
const s = result as WebflowSite;
|
|
202
|
+
if (s && typeof s === 'object' && 'id' in s && !('error' in s)) {
|
|
203
|
+
return {
|
|
204
|
+
id: s.id,
|
|
205
|
+
displayName: s.displayName,
|
|
206
|
+
shortName: s.shortName,
|
|
207
|
+
previewUrl: s.previewUrl,
|
|
208
|
+
timeZone: s.timeZone,
|
|
209
|
+
lastPublished: s.lastPublished,
|
|
210
|
+
customDomains: s.customDomains,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
case 'list_collections': {
|
|
216
|
+
const siteId = args.site_id as string;
|
|
217
|
+
const result = await wFetch(
|
|
218
|
+
context,
|
|
219
|
+
`${API}/sites/${encodeURIComponent(siteId)}/collections`,
|
|
220
|
+
);
|
|
221
|
+
const collections = (result as { collections?: WebflowCollection[] }).collections;
|
|
222
|
+
if (Array.isArray(collections)) {
|
|
223
|
+
return collections.map((c) => ({
|
|
224
|
+
id: c.id,
|
|
225
|
+
displayName: c.displayName,
|
|
226
|
+
slug: c.slug,
|
|
227
|
+
singularName: c.singularName,
|
|
228
|
+
createdOn: c.createdOn,
|
|
229
|
+
lastUpdated: c.lastUpdated,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
case 'list_collection_items': {
|
|
235
|
+
const collectionId = args.collection_id as string;
|
|
236
|
+
const limit = Math.min(100, Math.max(1, (args.limit as number) ?? 50));
|
|
237
|
+
const offset = Math.max(0, (args.offset as number) ?? 0);
|
|
238
|
+
const params = new URLSearchParams({
|
|
239
|
+
limit: String(limit),
|
|
240
|
+
offset: String(offset),
|
|
241
|
+
});
|
|
242
|
+
const result = await wFetch(
|
|
243
|
+
context,
|
|
244
|
+
`${API}/collections/${encodeURIComponent(collectionId)}/items?${params}`,
|
|
245
|
+
);
|
|
246
|
+
const items = (result as { items?: WebflowItem[] }).items;
|
|
247
|
+
if (Array.isArray(items)) {
|
|
248
|
+
return {
|
|
249
|
+
pagination: (result as { pagination?: unknown }).pagination,
|
|
250
|
+
items: items.map((i) => ({
|
|
251
|
+
id: i.id,
|
|
252
|
+
isDraft: i.isDraft,
|
|
253
|
+
isArchived: i.isArchived,
|
|
254
|
+
lastPublished: i.lastPublished,
|
|
255
|
+
fieldData: i.fieldData,
|
|
256
|
+
})),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
case 'get_collection_item': {
|
|
262
|
+
const collectionId = args.collection_id as string;
|
|
263
|
+
const itemId = args.item_id as string;
|
|
264
|
+
const result = await wFetch(
|
|
265
|
+
context,
|
|
266
|
+
`${API}/collections/${encodeURIComponent(collectionId)}/items/${encodeURIComponent(itemId)}`,
|
|
267
|
+
);
|
|
268
|
+
const i = result as WebflowItem;
|
|
269
|
+
if (i && typeof i === 'object' && 'id' in i && !('error' in i)) {
|
|
270
|
+
return {
|
|
271
|
+
id: i.id,
|
|
272
|
+
isDraft: i.isDraft,
|
|
273
|
+
isArchived: i.isArchived,
|
|
274
|
+
lastPublished: i.lastPublished,
|
|
275
|
+
fieldData: i.fieldData,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export default { tools, callTool, meter: { credits: 1 }, provider: 'webflow' } satisfies McpToolExport;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|