@curenorway/kode-mcp 1.0.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/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # Cure Kode MCP Server
2
+
3
+ MCP (Model Context Protocol) server that enables AI agents to manage Webflow scripts via Cure Kode CDN.
4
+
5
+ ## What is this?
6
+
7
+ This MCP server allows AI assistants like Claude to directly interact with Cure Kode:
8
+ - List, create, update, and delete scripts
9
+ - Deploy to staging and production
10
+ - Check deployment status
11
+ - Analyze web pages for script detection
12
+
13
+ ## Installation
14
+
15
+ ### For Claude Code
16
+
17
+ Add to your Claude Code configuration (`~/.claude/claude_code_config.json`):
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "cure-kode": {
23
+ "command": "npx",
24
+ "args": ["@curenorway/kode-mcp"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### For Cursor / Other IDEs
31
+
32
+ Refer to your IDE's MCP configuration documentation.
33
+
34
+ ## Configuration
35
+
36
+ The MCP server needs to know which site to manage. Configure via:
37
+
38
+ ### Option 1: Project Config (Recommended)
39
+
40
+ Run `kode init` in your project to create `.cure-kode/config.json`:
41
+
42
+ ```json
43
+ {
44
+ "siteId": "your-site-uuid",
45
+ "siteSlug": "my-site",
46
+ "siteName": "My Site",
47
+ "apiKey": "ck_...",
48
+ "scriptsDir": "kode",
49
+ "environment": "staging"
50
+ }
51
+ ```
52
+
53
+ ### Option 2: Environment Variables
54
+
55
+ ```bash
56
+ export CURE_KODE_API_KEY="ck_..."
57
+ export CURE_KODE_SITE_ID="your-site-uuid"
58
+ export CURE_KODE_API_URL="https://cure-app-v2-production.up.railway.app" # optional
59
+ ```
60
+
61
+ ## Available Tools
62
+
63
+ | Tool | Description |
64
+ |------|-------------|
65
+ | `kode_list_scripts` | List all scripts for the site |
66
+ | `kode_get_script` | Get script content by slug |
67
+ | `kode_create_script` | Create a new script |
68
+ | `kode_update_script` | Update script content |
69
+ | `kode_delete_script` | Delete a script |
70
+ | `kode_deploy` | Deploy to staging/production |
71
+ | `kode_promote` | Promote staging to production |
72
+ | `kode_status` | Get deployment status |
73
+ | `kode_fetch_html` | Analyze a webpage |
74
+ | `kode_list_pages` | List page definitions |
75
+ | `kode_site_info` | Get site info and CDN URL |
76
+
77
+ ## Example Prompts
78
+
79
+ Once configured, you can ask Claude:
80
+
81
+ ```
82
+ "List all the scripts in Cure Kode"
83
+ "Create a new tracking script that logs page views"
84
+ "Update the init.js script to add error handling"
85
+ "Deploy to staging"
86
+ "What's the current deployment status?"
87
+ "Analyze example.com to see what scripts are loaded"
88
+ ```
89
+
90
+ ## How It Works
91
+
92
+ ```
93
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
94
+ │ AI Agent │ MCP │ Kode MCP │ REST │ Cure Kode API │
95
+ │ (Claude/etc) │─────▶│ Server │─────▶│ (Railway) │
96
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
97
+
98
+
99
+ ┌─────────────────┐
100
+ │ Local Scripts │
101
+ │ (.cure-kode/) │
102
+ └─────────────────┘
103
+ ```
104
+
105
+ 1. AI agent calls MCP tools (e.g., `kode_create_script`)
106
+ 2. MCP server reads config from `.cure-kode/config.json`
107
+ 3. MCP server calls Cure Kode REST API with API key
108
+ 4. Results returned to AI agent
109
+
110
+ ## Security
111
+
112
+ - API keys are stored locally in `.cure-kode/config.json` (git-ignored)
113
+ - Keys are scoped per-site with specific permissions
114
+ - All API calls are authenticated
115
+ - Never commit API keys to version control
116
+
117
+ ## Troubleshooting
118
+
119
+ ### "Cure Kode not configured"
120
+
121
+ Either:
122
+ 1. Run `kode init` in your project directory
123
+ 2. Set `CURE_KODE_API_KEY` and `CURE_KODE_SITE_ID` environment variables
124
+
125
+ ### "API key invalid"
126
+
127
+ - Check your API key starts with `ck_`
128
+ - Verify the key hasn't expired
129
+ - Ensure the key has required permissions
130
+
131
+ ### Tools not appearing in Claude
132
+
133
+ - Restart Claude Code after configuration changes
134
+ - Check the MCP server is running: `npx @curenorway/kode-mcp`
135
+ - Verify config file syntax
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ # Install dependencies
141
+ pnpm install
142
+
143
+ # Build
144
+ pnpm build
145
+
146
+ # Run locally
147
+ node dist/index.js
148
+ ```
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,730 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema,
9
+ ListResourcesRequestSchema,
10
+ ReadResourceRequestSchema
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+
13
+ // src/api.ts
14
+ var KodeApiClient = class {
15
+ apiUrl;
16
+ apiKey;
17
+ constructor(apiUrl, apiKey) {
18
+ this.apiUrl = apiUrl.replace(/\/$/, "");
19
+ this.apiKey = apiKey;
20
+ }
21
+ async request(path3, options = {}) {
22
+ const url = `${this.apiUrl}${path3}`;
23
+ const headers = {
24
+ "Content-Type": "application/json",
25
+ "X-API-Key": this.apiKey,
26
+ ...options.headers
27
+ };
28
+ const response = await fetch(url, {
29
+ ...options,
30
+ headers
31
+ });
32
+ if (!response.ok) {
33
+ const error = await response.json().catch(() => ({ error: response.statusText }));
34
+ throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`);
35
+ }
36
+ return response.json();
37
+ }
38
+ // Site operations
39
+ async getSite(siteId) {
40
+ return this.request(`/api/cdn/sites/${siteId}`);
41
+ }
42
+ async listSites() {
43
+ return this.request("/api/cdn/sites");
44
+ }
45
+ // Script operations
46
+ async listScripts(siteId) {
47
+ return this.request(`/api/cdn/sites/${siteId}/scripts`);
48
+ }
49
+ async getScript(scriptId) {
50
+ return this.request(`/api/cdn/scripts/${scriptId}`);
51
+ }
52
+ async createScript(siteId, data) {
53
+ return this.request(`/api/cdn/sites/${siteId}/scripts`, {
54
+ method: "POST",
55
+ body: JSON.stringify(data)
56
+ });
57
+ }
58
+ async updateScript(scriptId, data) {
59
+ return this.request(`/api/cdn/scripts/${scriptId}`, {
60
+ method: "PUT",
61
+ body: JSON.stringify(data)
62
+ });
63
+ }
64
+ async deleteScript(scriptId) {
65
+ await this.request(`/api/cdn/scripts/${scriptId}`, {
66
+ method: "DELETE"
67
+ });
68
+ }
69
+ // Page operations
70
+ async listPages(siteId) {
71
+ return this.request(`/api/cdn/sites/${siteId}/pages`);
72
+ }
73
+ // Deployment operations
74
+ async deploy(siteId, options = {}) {
75
+ return this.request("/api/cdn/deploy", {
76
+ method: "POST",
77
+ body: JSON.stringify({
78
+ siteId,
79
+ environment: options.environment || "staging",
80
+ scriptIds: options.scriptIds,
81
+ notes: options.notes
82
+ })
83
+ });
84
+ }
85
+ async promoteToProduction(siteId, stagingDeploymentId) {
86
+ return this.request("/api/cdn/deploy/promote", {
87
+ method: "POST",
88
+ body: JSON.stringify({
89
+ siteId,
90
+ stagingDeploymentId
91
+ })
92
+ });
93
+ }
94
+ async getDeploymentStatus(siteId) {
95
+ return this.request(`/api/cdn/sites/${siteId}/deployments/status`);
96
+ }
97
+ // HTML operations
98
+ async fetchHtml(siteId, url) {
99
+ return this.request("/api/cdn/fetch-html", {
100
+ method: "POST",
101
+ body: JSON.stringify({ siteId, url })
102
+ });
103
+ }
104
+ // Validate API key
105
+ async validateKey() {
106
+ try {
107
+ await this.listSites();
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+ };
114
+
115
+ // src/config.ts
116
+ import * as fs from "fs";
117
+ import * as path from "path";
118
+ var DEFAULT_API_URL = "https://cure-app-v2-production.up.railway.app";
119
+ var CONFIG_DIR = ".cure-kode";
120
+ var CONFIG_FILE = "config.json";
121
+ function findProjectRoot(startDir = process.cwd()) {
122
+ let currentDir = startDir;
123
+ while (currentDir !== path.dirname(currentDir)) {
124
+ const configPath = path.join(currentDir, CONFIG_DIR, CONFIG_FILE);
125
+ if (fs.existsSync(configPath)) {
126
+ return currentDir;
127
+ }
128
+ currentDir = path.dirname(currentDir);
129
+ }
130
+ return void 0;
131
+ }
132
+ function loadProjectConfig() {
133
+ const projectRoot = findProjectRoot();
134
+ if (!projectRoot) return void 0;
135
+ const configPath = path.join(projectRoot, CONFIG_DIR, CONFIG_FILE);
136
+ if (!fs.existsSync(configPath)) return void 0;
137
+ try {
138
+ const content = fs.readFileSync(configPath, "utf-8");
139
+ return JSON.parse(content);
140
+ } catch {
141
+ return void 0;
142
+ }
143
+ }
144
+ function getConfig() {
145
+ const envApiKey = process.env.CURE_KODE_API_KEY;
146
+ const envApiUrl = process.env.CURE_KODE_API_URL;
147
+ const envSiteId = process.env.CURE_KODE_SITE_ID;
148
+ const projectConfig = loadProjectConfig();
149
+ const apiKey = envApiKey || projectConfig?.apiKey;
150
+ if (!apiKey) {
151
+ return void 0;
152
+ }
153
+ const siteId = envSiteId || projectConfig?.siteId;
154
+ if (!siteId) {
155
+ return void 0;
156
+ }
157
+ return {
158
+ apiKey,
159
+ siteId,
160
+ siteSlug: projectConfig?.siteSlug || "unknown",
161
+ siteName: projectConfig?.siteName || "Unknown Site",
162
+ apiUrl: envApiUrl || projectConfig?.apiUrl || DEFAULT_API_URL
163
+ };
164
+ }
165
+ function hasConfig() {
166
+ return getConfig() !== void 0;
167
+ }
168
+ function getScriptsDir() {
169
+ const projectRoot = findProjectRoot();
170
+ const projectConfig = loadProjectConfig();
171
+ if (!projectRoot || !projectConfig) return void 0;
172
+ return path.join(projectRoot, projectConfig.scriptsDir);
173
+ }
174
+
175
+ // src/index.ts
176
+ import * as fs2 from "fs";
177
+ import * as path2 from "path";
178
+ var server = new Server(
179
+ {
180
+ name: "cure-kode",
181
+ version: "1.0.0"
182
+ },
183
+ {
184
+ capabilities: {
185
+ tools: {},
186
+ resources: {}
187
+ }
188
+ }
189
+ );
190
+ var apiClient = null;
191
+ function getApiClient() {
192
+ if (!apiClient) {
193
+ const config = getConfig();
194
+ if (!config) {
195
+ throw new Error(
196
+ "Cure Kode not configured. Set CURE_KODE_API_KEY and CURE_KODE_SITE_ID environment variables, or run from a directory with .cure-kode/config.json (use `kode init` to create one)."
197
+ );
198
+ }
199
+ apiClient = new KodeApiClient(config.apiUrl, config.apiKey);
200
+ }
201
+ return apiClient;
202
+ }
203
+ function getSiteId() {
204
+ const config = getConfig();
205
+ if (!config) {
206
+ throw new Error("Cure Kode not configured");
207
+ }
208
+ return config.siteId;
209
+ }
210
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
211
+ return {
212
+ tools: [
213
+ {
214
+ name: "kode_list_scripts",
215
+ description: "List all scripts for the current Cure Kode site. Returns script names, types (javascript/css), scopes (global/page-specific), and version numbers.",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {},
219
+ required: []
220
+ }
221
+ },
222
+ {
223
+ name: "kode_get_script",
224
+ description: "Get the full content of a specific script by its slug (filename without extension) or ID.",
225
+ inputSchema: {
226
+ type: "object",
227
+ properties: {
228
+ slug: {
229
+ type: "string",
230
+ description: 'Script slug (e.g., "init", "tracking") or script ID (UUID)'
231
+ }
232
+ },
233
+ required: ["slug"]
234
+ }
235
+ },
236
+ {
237
+ name: "kode_create_script",
238
+ description: "Create a new script on the Cure Kode CDN. The script will be available at the CDN URL after deployment.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ name: {
243
+ type: "string",
244
+ description: 'Script name/slug (lowercase, hyphens allowed, e.g., "my-script")'
245
+ },
246
+ type: {
247
+ type: "string",
248
+ enum: ["javascript", "css"],
249
+ description: "Script type"
250
+ },
251
+ content: {
252
+ type: "string",
253
+ description: "Script content (JavaScript or CSS code)"
254
+ },
255
+ scope: {
256
+ type: "string",
257
+ enum: ["global", "page-specific"],
258
+ description: 'Script scope - "global" loads on all pages, "page-specific" only on assigned pages. Default: global'
259
+ }
260
+ },
261
+ required: ["name", "type", "content"]
262
+ }
263
+ },
264
+ {
265
+ name: "kode_update_script",
266
+ description: "Update an existing script's content. This creates a new version of the script.",
267
+ inputSchema: {
268
+ type: "object",
269
+ properties: {
270
+ slug: {
271
+ type: "string",
272
+ description: "Script slug or ID to update"
273
+ },
274
+ content: {
275
+ type: "string",
276
+ description: "New script content"
277
+ },
278
+ changeSummary: {
279
+ type: "string",
280
+ description: "Brief description of the changes (for version history)"
281
+ }
282
+ },
283
+ required: ["slug", "content"]
284
+ }
285
+ },
286
+ {
287
+ name: "kode_delete_script",
288
+ description: "Delete a script from the Cure Kode CDN. This cannot be undone.",
289
+ inputSchema: {
290
+ type: "object",
291
+ properties: {
292
+ slug: {
293
+ type: "string",
294
+ description: "Script slug or ID to delete"
295
+ }
296
+ },
297
+ required: ["slug"]
298
+ }
299
+ },
300
+ {
301
+ name: "kode_deploy",
302
+ description: "Deploy scripts to staging or production environment. Always deploy to staging first and test before promoting to production.",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ environment: {
307
+ type: "string",
308
+ enum: ["staging", "production"],
309
+ description: "Target environment. Default: staging"
310
+ },
311
+ notes: {
312
+ type: "string",
313
+ description: 'Deployment notes (e.g., "Added new tracking script")'
314
+ }
315
+ },
316
+ required: []
317
+ }
318
+ },
319
+ {
320
+ name: "kode_promote",
321
+ description: "Promote the latest staging deployment to production. Use this after testing on staging.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {},
325
+ required: []
326
+ }
327
+ },
328
+ {
329
+ name: "kode_status",
330
+ description: "Get the current deployment status for both staging and production environments.",
331
+ inputSchema: {
332
+ type: "object",
333
+ properties: {},
334
+ required: []
335
+ }
336
+ },
337
+ {
338
+ name: "kode_fetch_html",
339
+ description: "Fetch and analyze a webpage to see what scripts are loaded, detect Webflow components, and check if Cure Kode is installed.",
340
+ inputSchema: {
341
+ type: "object",
342
+ properties: {
343
+ url: {
344
+ type: "string",
345
+ description: 'Full URL to analyze (e.g., "https://example.com")'
346
+ }
347
+ },
348
+ required: ["url"]
349
+ }
350
+ },
351
+ {
352
+ name: "kode_list_pages",
353
+ description: "List all page definitions for the site. Pages define URL patterns for page-specific scripts.",
354
+ inputSchema: {
355
+ type: "object",
356
+ properties: {},
357
+ required: []
358
+ }
359
+ },
360
+ {
361
+ name: "kode_site_info",
362
+ description: "Get information about the current Cure Kode site including domains and CDN URLs.",
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {},
366
+ required: []
367
+ }
368
+ }
369
+ ]
370
+ };
371
+ });
372
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
373
+ const { name, arguments: args } = request.params;
374
+ try {
375
+ const client = getApiClient();
376
+ const siteId = getSiteId();
377
+ switch (name) {
378
+ case "kode_list_scripts": {
379
+ const scripts = await client.listScripts(siteId);
380
+ const formatted = scripts.map((s) => ({
381
+ slug: s.slug,
382
+ name: s.name,
383
+ type: s.type,
384
+ scope: s.scope,
385
+ version: s.current_version,
386
+ active: s.is_active,
387
+ loadOrder: s.load_order
388
+ }));
389
+ return {
390
+ content: [
391
+ {
392
+ type: "text",
393
+ text: JSON.stringify(formatted, null, 2)
394
+ }
395
+ ]
396
+ };
397
+ }
398
+ case "kode_get_script": {
399
+ const { slug } = args;
400
+ const scripts = await client.listScripts(siteId);
401
+ const script = scripts.find((s) => s.slug === slug || s.id === slug);
402
+ if (!script) {
403
+ return {
404
+ content: [{ type: "text", text: `Script "${slug}" not found` }],
405
+ isError: true
406
+ };
407
+ }
408
+ return {
409
+ content: [
410
+ {
411
+ type: "text",
412
+ text: `// Script: ${script.name} (${script.type})
413
+ // Version: ${script.current_version}
414
+ // Scope: ${script.scope}
415
+
416
+ ${script.content}`
417
+ }
418
+ ]
419
+ };
420
+ }
421
+ case "kode_create_script": {
422
+ const { name: scriptName, type, content, scope } = args;
423
+ const script = await client.createScript(siteId, {
424
+ name: scriptName,
425
+ slug: scriptName,
426
+ type,
427
+ content,
428
+ scope: scope || "global"
429
+ });
430
+ return {
431
+ content: [
432
+ {
433
+ type: "text",
434
+ text: `Created script "${script.name}" (${script.type})
435
+ Slug: ${script.slug}
436
+ Version: ${script.current_version}
437
+
438
+ Note: Run kode_deploy to make it live.`
439
+ }
440
+ ]
441
+ };
442
+ }
443
+ case "kode_update_script": {
444
+ const { slug, content, changeSummary } = args;
445
+ const scripts = await client.listScripts(siteId);
446
+ const script = scripts.find((s) => s.slug === slug || s.id === slug);
447
+ if (!script) {
448
+ return {
449
+ content: [{ type: "text", text: `Script "${slug}" not found` }],
450
+ isError: true
451
+ };
452
+ }
453
+ const updated = await client.updateScript(script.id, {
454
+ content,
455
+ changeSummary: changeSummary || "Updated via MCP"
456
+ });
457
+ return {
458
+ content: [
459
+ {
460
+ type: "text",
461
+ text: `Updated script "${updated.name}"
462
+ New version: ${updated.current_version}
463
+
464
+ Note: Run kode_deploy to make changes live.`
465
+ }
466
+ ]
467
+ };
468
+ }
469
+ case "kode_delete_script": {
470
+ const { slug } = args;
471
+ const scripts = await client.listScripts(siteId);
472
+ const script = scripts.find((s) => s.slug === slug || s.id === slug);
473
+ if (!script) {
474
+ return {
475
+ content: [{ type: "text", text: `Script "${slug}" not found` }],
476
+ isError: true
477
+ };
478
+ }
479
+ await client.deleteScript(script.id);
480
+ return {
481
+ content: [
482
+ {
483
+ type: "text",
484
+ text: `Deleted script "${script.name}"`
485
+ }
486
+ ]
487
+ };
488
+ }
489
+ case "kode_deploy": {
490
+ const { environment, notes } = args;
491
+ const deployment = await client.deploy(siteId, {
492
+ environment: environment || "staging",
493
+ notes: notes || "Deployed via MCP"
494
+ });
495
+ return {
496
+ content: [
497
+ {
498
+ type: "text",
499
+ text: `Deployment ${deployment.version} to ${deployment.environment}: ${deployment.status}
500
+ Started: ${deployment.started_at}${deployment.completed_at ? `
501
+ Completed: ${deployment.completed_at}` : ""}`
502
+ }
503
+ ]
504
+ };
505
+ }
506
+ case "kode_promote": {
507
+ const status = await client.getDeploymentStatus(siteId);
508
+ if (!status.canPromote) {
509
+ return {
510
+ content: [
511
+ {
512
+ type: "text",
513
+ text: "Cannot promote: No staging deployment to promote, or staging and production are already in sync."
514
+ }
515
+ ],
516
+ isError: true
517
+ };
518
+ }
519
+ const deployment = await client.promoteToProduction(siteId);
520
+ return {
521
+ content: [
522
+ {
523
+ type: "text",
524
+ text: `Promoted ${deployment.version} to production
525
+ Status: ${deployment.status}`
526
+ }
527
+ ]
528
+ };
529
+ }
530
+ case "kode_status": {
531
+ const status = await client.getDeploymentStatus(siteId);
532
+ const config = getConfig();
533
+ let text = `Site: ${config?.siteName || "Unknown"}
534
+
535
+ `;
536
+ text += `STAGING:
537
+ `;
538
+ if (status.staging.lastSuccessful) {
539
+ text += ` Version: ${status.staging.lastSuccessful.version}
540
+ `;
541
+ text += ` Deployed: ${status.staging.lastSuccessful.completed_at}
542
+ `;
543
+ } else {
544
+ text += ` Not deployed
545
+ `;
546
+ }
547
+ text += `
548
+ PRODUCTION:
549
+ `;
550
+ if (status.production.lastSuccessful) {
551
+ text += ` Version: ${status.production.lastSuccessful.version}
552
+ `;
553
+ text += ` Deployed: ${status.production.lastSuccessful.completed_at}
554
+ `;
555
+ } else {
556
+ text += ` Not deployed
557
+ `;
558
+ }
559
+ text += `
560
+ Can promote staging to production: ${status.canPromote ? "Yes" : "No"}`;
561
+ return {
562
+ content: [{ type: "text", text }]
563
+ };
564
+ }
565
+ case "kode_fetch_html": {
566
+ const { url } = args;
567
+ const result = await client.fetchHtml(siteId, url);
568
+ let text = `URL: ${result.url}
569
+ Title: ${result.title}
570
+
571
+ `;
572
+ text += `Webflow: ${result.hasWebflow ? "Yes" : "No"}
573
+ `;
574
+ text += `Cure Kode: ${result.hasCureKode ? `Yes (${result.cureKodeVersion || "version unknown"})` : "No"}
575
+
576
+ `;
577
+ if (result.scripts.length > 0) {
578
+ text += `Scripts (${result.scripts.length}):
579
+ `;
580
+ for (const script of result.scripts) {
581
+ text += ` [${script.type}] ${script.src || "(inline)"} - ${script.position}
582
+ `;
583
+ }
584
+ }
585
+ if (result.stylesheets.length > 0) {
586
+ text += `
587
+ Stylesheets (${result.stylesheets.length}):
588
+ `;
589
+ for (const style of result.stylesheets) {
590
+ text += ` [${style.type}] ${style.href || "(inline)"}
591
+ `;
592
+ }
593
+ }
594
+ if (result.webflowComponents.length > 0) {
595
+ text += `
596
+ Webflow Components:
597
+ `;
598
+ for (const comp of result.webflowComponents) {
599
+ text += ` - ${comp}
600
+ `;
601
+ }
602
+ }
603
+ return {
604
+ content: [{ type: "text", text }]
605
+ };
606
+ }
607
+ case "kode_list_pages": {
608
+ const pages = await client.listPages(siteId);
609
+ if (pages.length === 0) {
610
+ return {
611
+ content: [{ type: "text", text: 'No pages defined. All scripts with scope "global" will load on all pages.' }]
612
+ };
613
+ }
614
+ const formatted = pages.map((p) => ({
615
+ name: p.name,
616
+ slug: p.slug,
617
+ patterns: p.url_patterns,
618
+ patternType: p.pattern_type,
619
+ priority: p.priority
620
+ }));
621
+ return {
622
+ content: [
623
+ {
624
+ type: "text",
625
+ text: JSON.stringify(formatted, null, 2)
626
+ }
627
+ ]
628
+ };
629
+ }
630
+ case "kode_site_info": {
631
+ const config = getConfig();
632
+ if (!config) {
633
+ return {
634
+ content: [{ type: "text", text: "Cure Kode not configured" }],
635
+ isError: true
636
+ };
637
+ }
638
+ const site = await client.getSite(siteId);
639
+ let text = `Site: ${site.name}
640
+ `;
641
+ text += `Slug: ${site.slug}
642
+ `;
643
+ text += `ID: ${site.id}
644
+
645
+ `;
646
+ if (site.staging_domain) {
647
+ text += `Staging Domain: ${site.staging_domain}
648
+ `;
649
+ }
650
+ if (site.production_domain) {
651
+ text += `Production Domain: ${site.production_domain}
652
+ `;
653
+ }
654
+ if (site.domain) {
655
+ text += `Domain: ${site.domain}
656
+ `;
657
+ }
658
+ text += `
659
+ CDN URL: ${config.apiUrl}/api/cdn/${site.slug}/init.js
660
+ `;
661
+ text += `
662
+ Embed code:
663
+ <script src="${config.apiUrl}/api/cdn/${site.slug}/init.js"></script>`;
664
+ return {
665
+ content: [{ type: "text", text }]
666
+ };
667
+ }
668
+ default:
669
+ return {
670
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
671
+ isError: true
672
+ };
673
+ }
674
+ } catch (error) {
675
+ return {
676
+ content: [
677
+ {
678
+ type: "text",
679
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
680
+ }
681
+ ],
682
+ isError: true
683
+ };
684
+ }
685
+ });
686
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
687
+ const scriptsDir = getScriptsDir();
688
+ if (!scriptsDir || !fs2.existsSync(scriptsDir)) {
689
+ return { resources: [] };
690
+ }
691
+ const files = fs2.readdirSync(scriptsDir);
692
+ const resources = files.filter((f) => f.endsWith(".js") || f.endsWith(".css")).map((f) => ({
693
+ uri: `file://${path2.join(scriptsDir, f)}`,
694
+ name: f,
695
+ mimeType: f.endsWith(".js") ? "application/javascript" : "text/css"
696
+ }));
697
+ return { resources };
698
+ });
699
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
700
+ const { uri } = request.params;
701
+ if (!uri.startsWith("file://")) {
702
+ throw new Error("Invalid resource URI");
703
+ }
704
+ const filePath = uri.replace("file://", "");
705
+ if (!fs2.existsSync(filePath)) {
706
+ throw new Error("File not found");
707
+ }
708
+ const content = fs2.readFileSync(filePath, "utf-8");
709
+ const mimeType = filePath.endsWith(".js") ? "application/javascript" : "text/css";
710
+ return {
711
+ contents: [
712
+ {
713
+ uri,
714
+ mimeType,
715
+ text: content
716
+ }
717
+ ]
718
+ };
719
+ });
720
+ async function main() {
721
+ if (!hasConfig()) {
722
+ console.error(
723
+ "Warning: Cure Kode not configured. Set CURE_KODE_API_KEY and CURE_KODE_SITE_ID, or run from a directory with .cure-kode/config.json"
724
+ );
725
+ }
726
+ const transport = new StdioServerTransport();
727
+ await server.connect(transport);
728
+ console.error("Cure Kode MCP server started");
729
+ }
730
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@curenorway/kode-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Cure Kode - enables AI agents to manage Webflow scripts",
5
+ "type": "module",
6
+ "bin": {
7
+ "cure-kode-mcp": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup src/index.ts --format esm --dts --clean",
16
+ "dev": "tsup src/index.ts --format esm --dts --watch",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "pnpm build"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.10.0",
25
+ "tsup": "^8.0.1",
26
+ "typescript": "^5.3.2"
27
+ },
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "keywords": [
32
+ "cure",
33
+ "kode",
34
+ "mcp",
35
+ "ai",
36
+ "claude",
37
+ "webflow"
38
+ ],
39
+ "author": "Cure Norway",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/curenorway/cure-app-v2",
44
+ "directory": "packages/kode-mcp"
45
+ }
46
+ }