@curenorway/kode-mcp 1.2.0 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +183 -83
  2. package/dist/index.js +604 -46
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,42 +2,52 @@
2
2
 
3
3
  MCP (Model Context Protocol) server that enables AI agents to manage Webflow scripts via Cure Kode CDN.
4
4
 
5
- ## What is this?
5
+ ## Features
6
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
7
+ - **Script Management** - List, create, update, and delete scripts
8
+ - **Deployment Control** - Deploy to staging, promote to production, rollback
9
+ - **Page Context** - Cache and retrieve page structures for development
10
+ - **Script Analysis** - Auto-analyze scripts for metadata (selectors, triggers, dependencies)
11
+ - **Production Safety** - Explicit enable required, confirmation for promote
12
12
 
13
13
  ## Installation
14
14
 
15
15
  ### For Claude Code
16
16
 
17
- Add to your Claude Code configuration (`~/.claude/claude_code_config.json`):
17
+ The easiest way is to run `kode init` in your project, which creates `.mcp.json`:
18
18
 
19
19
  ```json
20
20
  {
21
21
  "mcpServers": {
22
22
  "cure-kode": {
23
+ "type": "stdio",
23
24
  "command": "npx",
24
- "args": ["@curenorway/kode-mcp"]
25
+ "args": ["-y", "@curenorway/kode-mcp"]
25
26
  }
26
27
  }
27
28
  }
28
29
  ```
29
30
 
30
- ### For Cursor / Other IDEs
31
+ Then restart Claude Code and approve the MCP when prompted.
31
32
 
32
- Refer to your IDE's MCP configuration documentation.
33
+ ### Manual Configuration
33
34
 
34
- ## Configuration
35
+ Add to your Claude Code configuration (`~/.claude/claude_code_config.json`):
35
36
 
36
- The MCP server needs to know which site to manage. Configure via:
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "cure-kode": {
41
+ "command": "npx",
42
+ "args": ["-y", "@curenorway/kode-mcp"]
43
+ }
44
+ }
45
+ }
46
+ ```
37
47
 
38
- ### Option 1: Project Config (Recommended)
48
+ ## Configuration
39
49
 
40
- Run `kode init` in your project to create `.cure-kode/config.json`:
50
+ The MCP server reads configuration from `.cure-kode/config.json` (created by `kode init`):
41
51
 
42
52
  ```json
43
53
  {
@@ -45,46 +55,151 @@ Run `kode init` in your project to create `.cure-kode/config.json`:
45
55
  "siteSlug": "my-site",
46
56
  "siteName": "My Site",
47
57
  "apiKey": "ck_...",
48
- "scriptsDir": "kode",
58
+ "scriptsDir": ".cure-kode-scripts",
49
59
  "environment": "staging"
50
60
  }
51
61
  ```
52
62
 
53
- ### Option 2: Environment Variables
54
-
63
+ Or via environment variables:
55
64
  ```bash
56
65
  export CURE_KODE_API_KEY="ck_..."
57
66
  export CURE_KODE_SITE_ID="your-site-uuid"
58
- export CURE_KODE_API_URL="https://app.cure.no" # optional
59
67
  ```
60
68
 
61
69
  ## Available Tools
62
70
 
71
+ ### Script Management
72
+
63
73
  | Tool | Description |
64
74
  |------|-------------|
65
75
  | `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 |
76
+ | `kode_get_script` | Get script by slug (supports `includeContent: false` for metadata only) |
77
+ | `kode_create_script` | Create a new script entry (metadata only, no content) |
78
+ | `kode_update_script` | Update script settings (autoLoad, scope, purpose) |
69
79
  | `kode_delete_script` | Delete a script |
70
- | `kode_deploy` | Deploy to staging/production |
71
- | `kode_promote` | Promote staging to production |
80
+ | `kode_push` | Upload local files from `.cure-kode-scripts/` to server |
81
+
82
+ ### Deployment
83
+
84
+ | Tool | Description |
85
+ |------|-------------|
86
+ | `kode_deploy` | Deploy to staging |
87
+ | `kode_promote` | Promote staging to production (**requires `confirmed: true`**) |
88
+ | `kode_rollback` | Rollback to previous deployment |
72
89
  | `kode_status` | Get deployment status |
73
- | `kode_fetch_html` | Analyze a webpage |
74
- | `kode_list_pages` | List page definitions |
90
+
91
+ ### Production Management
92
+
93
+ | Tool | Description |
94
+ |------|-------------|
95
+ | `kode_production_enable` | Enable production environment |
96
+ | `kode_production_disable` | Disable production environment |
97
+
98
+ ### Page & HTML Analysis
99
+
100
+ | Tool | Description |
101
+ |------|-------------|
102
+ | `kode_fetch_html` | Fetch and parse HTML from URL |
103
+ | `kode_fetch_html_smart` | Fetch with CMS truncation for smaller context |
104
+ | `kode_list_pages` | List page definitions for the site |
105
+ | `kode_assign_script_to_page` | Assign script to specific pages |
106
+ | `kode_remove_script_from_page` | Remove script from page |
107
+
108
+ ### Page Context (for AI Development)
109
+
110
+ | Tool | Description |
111
+ |------|-------------|
112
+ | `kode_refresh_page` | Fetch and cache page structure |
113
+ | `kode_get_page_context` | Get cached page context (sections, forms, CTAs, selectors) |
114
+ | `kode_list_pages_context` | List all cached page contexts |
115
+
116
+ ### Script Metadata
117
+
118
+ | Tool | Description |
119
+ |------|-------------|
120
+ | `kode_analyze_script` | Analyze script and generate metadata |
121
+ | `kode_get_script_metadata` | Get script metadata and AI summary |
122
+
123
+ ### Other
124
+
125
+ | Tool | Description |
126
+ |------|-------------|
75
127
  | `kode_site_info` | Get site info and CDN URL |
128
+ | `kode_read_context` | Read AI context file |
129
+ | `kode_update_context` | Update AI context file |
130
+
131
+ ## Important: Content Workflow
132
+
133
+ **MCP is the control plane - never send script content through MCP tools.**
134
+
135
+ The correct workflow:
136
+ 1. Write script file locally to `.cure-kode-scripts/script-name.js`
137
+ 2. Use `kode_push` to upload local files to server
138
+ 3. Use `kode_deploy` to deploy
139
+
140
+ ```
141
+ AI writes file → kode_push → kode_deploy
142
+ ```
143
+
144
+ **Do NOT** pass content to `kode_create_script` or `kode_update_script` - these only handle metadata.
145
+
146
+ ## Safety Features
147
+
148
+ ### Production Confirmation
149
+
150
+ `kode_promote` requires explicit confirmation to prevent accidental production deployments:
151
+
152
+ ```typescript
153
+ // This will fail:
154
+ kode_promote()
155
+
156
+ // This works:
157
+ kode_promote({ confirmed: true })
158
+ ```
159
+
160
+ ### Production Disabled by Default
161
+
162
+ New sites start with production disabled. Must explicitly enable:
163
+
164
+ ```typescript
165
+ kode_production_enable()
166
+ // Then can promote
167
+ kode_promote({ confirmed: true })
168
+ ```
169
+
170
+ ## Example Usage
76
171
 
77
- ## Example Prompts
172
+ ### Create and Deploy a Script
78
173
 
79
- Once configured, you can ask Claude:
174
+ ```
175
+ 1. "Create a new script called tracking.js"
176
+ → kode_create_script({ name: "tracking", slug: "tracking", type: "javascript" })
177
+
178
+ 2. Write the file locally
179
+ → Write to .cure-kode-scripts/tracking.js
180
+
181
+ 3. "Push the changes"
182
+ → kode_push()
183
+
184
+ 4. "Deploy to staging"
185
+ → kode_deploy()
186
+
187
+ 5. "Promote to production"
188
+ → kode_promote({ confirmed: true })
189
+ ```
190
+
191
+ ### Analyze a Page and Add Functionality
80
192
 
81
193
  ```
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"
194
+ 1. "Analyze the homepage"
195
+ kode_fetch_html_smart({ url: "https://mysite.com" })
196
+ kode_refresh_page({ url: "https://mysite.com" })
197
+
198
+ 2. "What sections are on this page?"
199
+ → kode_get_page_context({ url: "https://mysite.com" })
200
+
201
+ 3. "Add a script to animate the hero section"
202
+ → Create script, write file, push, deploy
88
203
  ```
89
204
 
90
205
  ## How It Works
@@ -92,88 +207,73 @@ Once configured, you can ask Claude:
92
207
  ```
93
208
  ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
94
209
  │ AI Agent │ MCP │ Kode MCP │ REST │ Cure Kode API │
95
- │ (Claude/etc) │─────▶│ Server │─────▶│ (Railway)
210
+ │ (Claude/etc) │─────▶│ Server │─────▶│ (app.cure.no)
96
211
  └─────────────────┘ └─────────────────┘ └─────────────────┘
97
212
 
98
213
 
99
214
  ┌─────────────────┐
100
215
  │ Local Scripts │
101
- (.cure-kode/)
216
+ │(.cure-kode-scripts/)│
102
217
  └─────────────────┘
103
218
  ```
104
219
 
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
220
  ## Security
111
221
 
112
222
  ### API Key Authentication
113
223
 
114
- - **SHA256 Hashed Storage**: API keys are hashed before storage on the server
224
+ - **Hashed Storage**: API keys are SHA256/HMAC hashed before server storage
115
225
  - **Site-scoped**: Each API key is bound to a specific CDN site
116
- - **Permission-based**: Keys have granular permissions:
117
- - `read` - List/view scripts and deployments
118
- - `write` - Create and update scripts
119
- - `deploy` - Deploy to staging/production
120
- - `delete` - Delete scripts
226
+ - **Permission-based**: Granular permissions (read, write, deploy, delete)
121
227
  - **Expiration**: Keys can have optional expiration dates
122
228
 
123
- ### Local Security
229
+ ### CORS Security (v2.6)
124
230
 
125
- - API keys stored in `.cure-kode/config.json` (auto-gitignored)
126
- - Never commit API keys to version control
127
- - Keys prefixed with `ck_` for easy identification
231
+ Script endpoints restrict CORS to configured domains:
232
+ - Site's domain, staging_domain, production_domain
233
+ - Webflow preview domains (*.webflow.io)
234
+ - Localhost for development
235
+ - **No wildcard CORS** on script endpoints
128
236
 
129
- ### Network Security
237
+ ### SSRF Protection
130
238
 
131
- - All API calls use HTTPS
132
- - **SSRF Protection**: HTML fetch endpoints block:
133
- - Private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
134
- - Localhost and internal hostnames
135
- - Cloud metadata endpoints (169.254.169.254)
136
- - Only HTTP/HTTPS protocols allowed
137
-
138
- ### Best Practices
139
-
140
- - Generate separate API keys for different environments/developers
141
- - Use read-only keys when full access isn't needed
142
- - Rotate keys periodically
143
- - Revoke keys when team members leave
239
+ HTML fetch endpoints block:
240
+ - Private IP ranges (127.0.0.0/8, 10.0.0.0/8, etc.)
241
+ - Cloud metadata endpoints (169.254.169.254)
242
+ - Internal hostnames
144
243
 
145
244
  ## Troubleshooting
146
245
 
147
246
  ### "Cure Kode not configured"
148
247
 
149
- Either:
150
- 1. Run `kode init` in your project directory
151
- 2. Set `CURE_KODE_API_KEY` and `CURE_KODE_SITE_ID` environment variables
248
+ Run `kode init` in your project directory to create configuration.
152
249
 
153
250
  ### "API key invalid"
154
251
 
155
- - Check your API key starts with `ck_`
156
- - Verify the key hasn't expired
157
- - Ensure the key has required permissions
252
+ - Check key starts with `ck_`
253
+ - Verify key hasn't expired
254
+ - Ensure key has required permissions
158
255
 
159
- ### Tools not appearing in Claude
256
+ ### "Production not enabled"
160
257
 
161
- - Restart Claude Code after configuration changes
162
- - Check the MCP server is running: `npx @curenorway/kode-mcp`
163
- - Verify config file syntax
258
+ Use `kode_production_enable()` before promoting.
164
259
 
165
- ## Development
260
+ ### "Promote requires confirmation"
166
261
 
167
- ```bash
168
- # Install dependencies
169
- pnpm install
262
+ Add `confirmed: true` parameter:
263
+ ```typescript
264
+ kode_promote({ confirmed: true })
265
+ ```
170
266
 
171
- # Build
172
- pnpm build
267
+ ### Tools not appearing in Claude
173
268
 
174
- # Run locally
175
- node dist/index.js
176
- ```
269
+ 1. Restart Claude Code after config changes
270
+ 2. Check `.mcp.json` syntax
271
+ 3. Verify MCP server runs: `npx @curenorway/kode-mcp`
272
+
273
+ ## Requirements
274
+
275
+ - Node.js 18 or later
276
+ - Cure Kode API key (from https://app.cure.no/tools/kode)
177
277
 
178
278
  ## License
179
279
 
package/dist/index.js CHANGED
@@ -105,6 +105,25 @@ var KodeApiClient = class {
105
105
  async getDeploymentStatus(siteId) {
106
106
  return this.request(`/api/cdn/sites/${siteId}/deployments/status`);
107
107
  }
108
+ async rollback(siteId, environment = "staging") {
109
+ return this.request("/api/cdn/deploy/rollback", {
110
+ method: "POST",
111
+ body: JSON.stringify({
112
+ siteId,
113
+ environment
114
+ })
115
+ });
116
+ }
117
+ // v2.3: Production enabled toggle
118
+ async setProductionEnabled(siteId, enabled, productionDomain) {
119
+ return this.request(`/api/cdn/sites/${siteId}/production`, {
120
+ method: "POST",
121
+ body: JSON.stringify({
122
+ enabled,
123
+ productionDomain
124
+ })
125
+ });
126
+ }
108
127
  // HTML operations
109
128
  async fetchHtml(siteId, url) {
110
129
  return this.request("/api/cdn/fetch-html", {
@@ -121,6 +140,16 @@ var KodeApiClient = class {
121
140
  return false;
122
141
  }
123
142
  }
143
+ // Metadata operations
144
+ async analyzeScript(scriptId, options = {}) {
145
+ return this.request(`/api/cdn/scripts/${scriptId}/analyze`, {
146
+ method: "POST",
147
+ body: JSON.stringify(options)
148
+ });
149
+ }
150
+ async getScriptMetadata(scriptId) {
151
+ return this.request(`/api/cdn/scripts/${scriptId}/metadata`);
152
+ }
124
153
  };
125
154
 
126
155
  // src/config.ts
@@ -274,7 +303,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
274
303
  },
275
304
  {
276
305
  name: "kode_create_script",
277
- description: "Create a new script on the Cure Kode CDN. The script will be available at the CDN URL after deployment. Global scripts auto-load by default; page-specific scripts do not.",
306
+ description: "Create a new script entry (metadata only). After creating, write the script file locally to .cure-kode-scripts/{name}.js and use kode_push to upload content. This keeps script content out of MCP context.",
278
307
  inputSchema: {
279
308
  type: "object",
280
309
  properties: {
@@ -287,10 +316,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
287
316
  enum: ["javascript", "css"],
288
317
  description: "Script type"
289
318
  },
290
- content: {
291
- type: "string",
292
- description: "Script content (JavaScript or CSS code)"
293
- },
294
319
  scope: {
295
320
  type: "string",
296
321
  enum: ["global", "page-specific"],
@@ -298,15 +323,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
298
323
  },
299
324
  autoLoad: {
300
325
  type: "boolean",
301
- description: "Whether to auto-load the script. Default: true for global scripts, false for page-specific. Set to false for scripts you want to load manually via CK.loadScript()."
326
+ description: "Whether to auto-load the script. Default: true for global scripts, false for page-specific."
327
+ },
328
+ purpose: {
329
+ type: "string",
330
+ description: "Brief description of what the script does (for metadata)"
331
+ }
332
+ },
333
+ required: ["name", "type"]
334
+ }
335
+ },
336
+ {
337
+ name: "kode_push",
338
+ description: "Push local script files to Cure Kode. Reads files from .cure-kode-scripts/ folder and uploads to API. This is the proper way to sync code - never send script content through other MCP tools.",
339
+ inputSchema: {
340
+ type: "object",
341
+ properties: {
342
+ scriptSlug: {
343
+ type: "string",
344
+ description: 'Specific script slug to push (e.g., "map"). If omitted, pushes all changed scripts.'
345
+ },
346
+ force: {
347
+ type: "boolean",
348
+ description: "Force push even if content appears unchanged. Default: false"
302
349
  }
303
350
  },
304
- required: ["name", "type", "content"]
351
+ required: []
305
352
  }
306
353
  },
307
354
  {
308
355
  name: "kode_update_script",
309
- description: "Update an existing script's content or settings. This creates a new version of the script when content changes.",
356
+ description: "Update script settings (NOT content). For content changes, edit the local file in .cure-kode-scripts/ and use kode_push. This tool is for metadata, scope, and autoLoad changes only.",
310
357
  inputSchema: {
311
358
  type: "object",
312
359
  properties: {
@@ -314,10 +361,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
314
361
  type: "string",
315
362
  description: "Script slug or ID to update"
316
363
  },
317
- content: {
318
- type: "string",
319
- description: "New script content"
320
- },
321
364
  autoLoad: {
322
365
  type: "boolean",
323
366
  description: "Whether to auto-load the script. Set to true to load automatically, false for manual loading via CK.loadScript()."
@@ -327,9 +370,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
327
370
  enum: ["global", "page-specific"],
328
371
  description: "Change script scope. Note: changing to page-specific requires assigning to pages."
329
372
  },
330
- changeSummary: {
373
+ purpose: {
331
374
  type: "string",
332
- description: "Brief description of the changes (for version history)"
375
+ description: "Update the script purpose description (shown in Chrome Extension)"
376
+ },
377
+ regenerateSummary: {
378
+ type: "boolean",
379
+ description: "Re-analyze the script content and regenerate the AI summary. Useful after significant changes."
333
380
  }
334
381
  },
335
382
  required: ["slug"]
@@ -370,16 +417,59 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
370
417
  },
371
418
  {
372
419
  name: "kode_promote",
373
- description: "Promote the latest staging deployment to production. Use this after testing on staging.",
420
+ description: "Promote the latest staging deployment to production. Use this after testing on staging. IMPORTANT: Requires confirmed=true to actually promote - this prevents accidental production deployments.",
374
421
  inputSchema: {
375
422
  type: "object",
376
- properties: {},
423
+ properties: {
424
+ confirmed: {
425
+ type: "boolean",
426
+ description: "Safety confirmation. Must be set to true to proceed with production promotion. If false or missing, returns a preview of what would be promoted."
427
+ }
428
+ },
429
+ required: []
430
+ }
431
+ },
432
+ {
433
+ name: "kode_rollback",
434
+ description: "Rollback to the previous deployment. Copies the previous version back to the active environment. Use this if a deployment caused issues.",
435
+ inputSchema: {
436
+ type: "object",
437
+ properties: {
438
+ environment: {
439
+ type: "string",
440
+ enum: ["staging", "production"],
441
+ description: "Environment to rollback. Default: staging"
442
+ }
443
+ },
377
444
  required: []
378
445
  }
379
446
  },
380
447
  {
381
448
  name: "kode_status",
382
- description: "Get the current deployment status for both staging and production environments.",
449
+ description: "Get the current deployment status including production enabled state, staging and production environments.",
450
+ inputSchema: {
451
+ type: "object",
452
+ properties: {},
453
+ required: []
454
+ }
455
+ },
456
+ {
457
+ name: "kode_production_enable",
458
+ description: "Enable production environment for this site. Required before promoting to production. Sites start in staging-only mode by default.",
459
+ inputSchema: {
460
+ type: "object",
461
+ properties: {
462
+ productionDomain: {
463
+ type: "string",
464
+ description: 'Production domain (e.g., "example.com"). Optional - can be set later.'
465
+ }
466
+ },
467
+ required: []
468
+ }
469
+ },
470
+ {
471
+ name: "kode_production_disable",
472
+ description: "Disable production environment for this site. Production requests will return an empty script. Useful during development when only staging should be active.",
383
473
  inputSchema: {
384
474
  type: "object",
385
475
  properties: {},
@@ -458,6 +548,38 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
458
548
  required: []
459
549
  }
460
550
  },
551
+ {
552
+ name: "kode_analyze_script",
553
+ description: "Analyze a script's content and auto-generate metadata. Detects DOM selectors, event triggers, dependencies (GSAP, Swiper, etc.), and behavior patterns. Optionally saves the analysis to the script.",
554
+ inputSchema: {
555
+ type: "object",
556
+ properties: {
557
+ slug: {
558
+ type: "string",
559
+ description: "Script slug or ID to analyze"
560
+ },
561
+ saveToScript: {
562
+ type: "boolean",
563
+ description: "Save the generated metadata to the script. Default: false (preview only)"
564
+ }
565
+ },
566
+ required: ["slug"]
567
+ }
568
+ },
569
+ {
570
+ name: "kode_get_script_metadata",
571
+ description: "Get the metadata for a script, including detected DOM targets, triggers, dependencies, and AI summary. Useful for Chrome Extension visibility.",
572
+ inputSchema: {
573
+ type: "object",
574
+ properties: {
575
+ slug: {
576
+ type: "string",
577
+ description: "Script slug or ID to get metadata for"
578
+ }
579
+ },
580
+ required: ["slug"]
581
+ }
582
+ },
461
583
  {
462
584
  name: "kode_read_context",
463
585
  description: "Read the project context file (.cure-kode/context.md). Contains current scripts, notes, and session history. ALWAYS call this before starting work on a Kode project.",
@@ -667,35 +789,155 @@ ${script.content}`
667
789
  };
668
790
  }
669
791
  case "kode_create_script": {
670
- const { name: scriptName, type, content, scope, autoLoad } = args;
792
+ const { name: scriptName, type, scope, autoLoad, purpose } = args;
671
793
  const scriptScope = scope || "global";
672
794
  const script = await client.createScript(siteId, {
673
795
  name: scriptName,
674
796
  slug: scriptName,
675
797
  type,
676
- content,
798
+ content: "",
799
+ // Empty - content comes from local files via kode_push
677
800
  scope: scriptScope,
678
- autoLoad
679
- // Let API handle default based on scope
801
+ autoLoad,
802
+ metadata: purpose ? { purpose } : void 0
680
803
  });
681
- const autoLoadStatus = script.auto_load ? "will auto-load" : "manual load only (use CK.loadScript())";
804
+ const scriptsDir = getScriptsDir();
805
+ const ext = type === "javascript" ? "js" : "css";
806
+ const localPath = scriptsDir ? `${scriptsDir}/${scriptName}.${ext}` : `.cure-kode-scripts/${scriptName}.${ext}`;
807
+ let responseText = `Created script "${script.name}" (${script.type})`;
808
+ responseText += `
809
+ Slug: ${script.slug}`;
810
+ responseText += `
811
+ Scope: ${script.scope}`;
812
+ responseText += `
813
+ Auto-load: ${script.auto_load ? "yes" : "no"}`;
814
+ if (purpose) responseText += `
815
+ Purpose: ${purpose}`;
816
+ responseText += `
817
+
818
+ Next steps:`;
819
+ responseText += `
820
+ 1. Create file: ${localPath}`;
821
+ responseText += `
822
+ 2. Write your ${type} code`;
823
+ responseText += `
824
+ 3. Run kode_push to upload content`;
825
+ responseText += `
826
+ 4. Run kode_deploy to make it live`;
682
827
  return {
683
828
  content: [
684
829
  {
685
830
  type: "text",
686
- text: `Created script "${script.name}" (${script.type})
687
- Slug: ${script.slug}
688
- Scope: ${script.scope}
689
- Auto-load: ${autoLoadStatus}
690
- Version: ${script.current_version}
691
-
692
- Note: Run kode_deploy to make it live.`
831
+ text: responseText
693
832
  }
694
833
  ]
695
834
  };
696
835
  }
836
+ case "kode_push": {
837
+ const { scriptSlug, force } = args;
838
+ const scriptsDir = getScriptsDir();
839
+ if (!scriptsDir || !fs2.existsSync(scriptsDir)) {
840
+ return {
841
+ content: [{
842
+ type: "text",
843
+ text: 'No .cure-kode-scripts/ folder found. Run "kode init" first or create the folder manually.'
844
+ }],
845
+ isError: true
846
+ };
847
+ }
848
+ const remoteScripts = await client.listScripts(siteId);
849
+ const localFiles = fs2.readdirSync(scriptsDir).filter((f) => f.endsWith(".js") || f.endsWith(".css"));
850
+ if (scriptSlug) {
851
+ const ext = remoteScripts.find((s) => s.slug === scriptSlug)?.type === "css" ? "css" : "js";
852
+ const fileName = `${scriptSlug}.${ext}`;
853
+ const filePath = path2.join(scriptsDir, fileName);
854
+ if (!fs2.existsSync(filePath)) {
855
+ return {
856
+ content: [{
857
+ type: "text",
858
+ text: `File not found: ${filePath}
859
+
860
+ Available files: ${localFiles.join(", ") || "(none)"}`
861
+ }],
862
+ isError: true
863
+ };
864
+ }
865
+ const content = fs2.readFileSync(filePath, "utf-8");
866
+ const remote = remoteScripts.find((s) => s.slug === scriptSlug);
867
+ if (!remote) {
868
+ return {
869
+ content: [{
870
+ type: "text",
871
+ text: `Script "${scriptSlug}" not found on server. Create it first with kode_create_script.`
872
+ }],
873
+ isError: true
874
+ };
875
+ }
876
+ if (!force && remote.content === content) {
877
+ return {
878
+ content: [{
879
+ type: "text",
880
+ text: `Script "${scriptSlug}" is already up to date (${content.length} chars). Use force: true to push anyway.`
881
+ }]
882
+ };
883
+ }
884
+ const updated = await client.updateScript(remote.id, {
885
+ content,
886
+ changeSummary: "Pushed via MCP"
887
+ });
888
+ return {
889
+ content: [{
890
+ type: "text",
891
+ text: `Pushed "${scriptSlug}": ${content.length} chars \u2192 v${updated.current_version}
892
+
893
+ Run kode_deploy to make changes live.`
894
+ }]
895
+ };
896
+ }
897
+ const results = [];
898
+ let pushedCount = 0;
899
+ let skippedCount = 0;
900
+ for (const file of localFiles) {
901
+ const slug = file.replace(/\.(js|css)$/, "");
902
+ const filePath = path2.join(scriptsDir, file);
903
+ const content = fs2.readFileSync(filePath, "utf-8");
904
+ const remote = remoteScripts.find((s) => s.slug === slug);
905
+ if (!remote) {
906
+ results.push(`\u26A0\uFE0F ${slug}: not on server (create with kode_create_script first)`);
907
+ continue;
908
+ }
909
+ if (!force && remote.content === content) {
910
+ skippedCount++;
911
+ continue;
912
+ }
913
+ try {
914
+ const updated = await client.updateScript(remote.id, {
915
+ content,
916
+ changeSummary: "Pushed via MCP"
917
+ });
918
+ results.push(`\u2713 ${slug}: ${content.length} chars \u2192 v${updated.current_version}`);
919
+ pushedCount++;
920
+ } catch (err) {
921
+ results.push(`\u2717 ${slug}: ${err instanceof Error ? err.message : "failed"}`);
922
+ }
923
+ }
924
+ let responseText = `Push complete: ${pushedCount} updated, ${skippedCount} unchanged`;
925
+ if (results.length > 0) {
926
+ responseText += `
927
+
928
+ ${results.join("\n")}`;
929
+ }
930
+ if (pushedCount > 0) {
931
+ responseText += `
932
+
933
+ Run kode_deploy to make changes live.`;
934
+ }
935
+ return {
936
+ content: [{ type: "text", text: responseText }]
937
+ };
938
+ }
697
939
  case "kode_update_script": {
698
- const { slug, content, autoLoad, scope, changeSummary } = args;
940
+ const { slug, autoLoad, scope, purpose, regenerateSummary } = args;
699
941
  const scripts = await client.listScripts(siteId);
700
942
  const script = scripts.find((s) => s.slug === slug || s.id === slug);
701
943
  if (!script) {
@@ -704,24 +946,32 @@ Note: Run kode_deploy to make it live.`
704
946
  isError: true
705
947
  };
706
948
  }
707
- const updateData = {};
708
- if (content !== void 0) updateData.content = content;
949
+ const updateData = {
950
+ changeSummary: "Updated settings via MCP"
951
+ };
709
952
  if (autoLoad !== void 0) updateData.autoLoad = autoLoad;
710
953
  if (scope !== void 0) updateData.scope = scope;
711
- updateData.changeSummary = changeSummary || "Updated via MCP";
954
+ if (purpose !== void 0) updateData.metadata = { purpose };
955
+ if (regenerateSummary !== void 0) updateData.regenerateSummary = regenerateSummary;
712
956
  const updated = await client.updateScript(script.id, updateData);
713
957
  const changes = [];
714
- if (content !== void 0) changes.push(`content \u2192 v${updated.current_version}`);
715
- if (autoLoad !== void 0) changes.push(`auto_load \u2192 ${autoLoad}`);
958
+ if (autoLoad !== void 0) changes.push(`autoLoad \u2192 ${autoLoad}`);
716
959
  if (scope !== void 0) changes.push(`scope \u2192 ${scope}`);
960
+ if (purpose !== void 0) changes.push(`purpose updated`);
961
+ if (regenerateSummary) changes.push("AI summary regenerated");
962
+ let responseText = `Updated script "${updated.name}"`;
963
+ if (changes.length > 0) {
964
+ responseText += `
965
+ Changes: ${changes.join(", ")}`;
966
+ }
967
+ responseText += `
968
+
969
+ To update content: edit local file and run kode_push`;
717
970
  return {
718
971
  content: [
719
972
  {
720
973
  type: "text",
721
- text: `Updated script "${updated.name}"
722
- Changes: ${changes.join(", ")}
723
-
724
- Note: Run kode_deploy to make changes live.`
974
+ text: responseText
725
975
  }
726
976
  ]
727
977
  };
@@ -752,19 +1002,55 @@ Note: Run kode_deploy to make changes live.`
752
1002
  environment: environment || "staging",
753
1003
  notes: notes || "Deployed via MCP"
754
1004
  });
1005
+ let responseText = `Deployment ${deployment.version} to ${deployment.environment}: ${deployment.status}`;
1006
+ responseText += `
1007
+ Started: ${deployment.started_at}`;
1008
+ if (deployment.completed_at) {
1009
+ responseText += `
1010
+ Completed: ${deployment.completed_at}`;
1011
+ }
1012
+ const scriptSizes = deployment.stats?.scriptSizes;
1013
+ if (scriptSizes && scriptSizes.length > 0) {
1014
+ responseText += `
1015
+
1016
+ Scripts deployed (${scriptSizes.length}):`;
1017
+ for (const s of scriptSizes) {
1018
+ const flags = [
1019
+ s.scope === "global" ? "G" : "P",
1020
+ s.autoLoad ? "\u26A1" : "\u25CB"
1021
+ ].join("");
1022
+ responseText += `
1023
+ ${s.slug} [${flags}]: ${s.contentSize} chars`;
1024
+ if (s.contentSize === 0) {
1025
+ responseText += " \u26A0\uFE0F EMPTY";
1026
+ } else if (s.contentSize < 50 && s.scope === "global") {
1027
+ responseText += " \u26A0\uFE0F very small";
1028
+ }
1029
+ }
1030
+ }
755
1031
  return {
756
1032
  content: [
757
1033
  {
758
1034
  type: "text",
759
- text: `Deployment ${deployment.version} to ${deployment.environment}: ${deployment.status}
760
- Started: ${deployment.started_at}${deployment.completed_at ? `
761
- Completed: ${deployment.completed_at}` : ""}`
1035
+ text: responseText
762
1036
  }
763
1037
  ]
764
1038
  };
765
1039
  }
766
1040
  case "kode_promote": {
1041
+ const { confirmed } = args;
767
1042
  const status = await client.getDeploymentStatus(siteId);
1043
+ if (!status.productionEnabled) {
1044
+ return {
1045
+ content: [
1046
+ {
1047
+ type: "text",
1048
+ text: "Cannot promote: Production is not enabled for this site.\n\nRun kode_production_enable first to activate the production environment."
1049
+ }
1050
+ ],
1051
+ isError: true
1052
+ };
1053
+ }
768
1054
  if (!status.canPromote) {
769
1055
  return {
770
1056
  content: [
@@ -776,23 +1062,109 @@ Completed: ${deployment.completed_at}` : ""}`
776
1062
  isError: true
777
1063
  };
778
1064
  }
1065
+ if (!confirmed) {
1066
+ const stagingVersion = status.staging.lastSuccessful?.version || "unknown";
1067
+ const productionVersion = status.production.lastSuccessful?.version || "(none)";
1068
+ return {
1069
+ content: [
1070
+ {
1071
+ type: "text",
1072
+ text: `\u26A0\uFE0F PRODUCTION PROMOTION CONFIRMATION REQUIRED
1073
+
1074
+ This will deploy to production:
1075
+ Staging version: ${stagingVersion}
1076
+ Current production: ${productionVersion}
1077
+
1078
+ To proceed, call kode_promote with confirmed: true
1079
+
1080
+ Note: Always test on staging first before promoting to production.`
1081
+ }
1082
+ ]
1083
+ };
1084
+ }
779
1085
  const deployment = await client.promoteToProduction(siteId);
780
1086
  return {
781
1087
  content: [
782
1088
  {
783
1089
  type: "text",
784
- text: `Promoted ${deployment.version} to production
1090
+ text: `\u2705 Promoted ${deployment.version} to production
785
1091
  Status: ${deployment.status}`
786
1092
  }
787
1093
  ]
788
1094
  };
789
1095
  }
1096
+ case "kode_rollback": {
1097
+ const { environment = "staging" } = args;
1098
+ if (environment === "production") {
1099
+ const status = await client.getDeploymentStatus(siteId);
1100
+ if (!status.productionEnabled) {
1101
+ return {
1102
+ content: [
1103
+ {
1104
+ type: "text",
1105
+ text: "Cannot rollback production: Production is not enabled for this site."
1106
+ }
1107
+ ],
1108
+ isError: true
1109
+ };
1110
+ }
1111
+ }
1112
+ const result = await client.rollback(siteId, environment);
1113
+ return {
1114
+ content: [
1115
+ {
1116
+ type: "text",
1117
+ text: `\u2705 Rolled back ${environment}
1118
+
1119
+ From: ${result.rolledBackFrom.version}
1120
+ To: ${result.rolledBackTo.version}
1121
+
1122
+ CDN URL: ${result.cdn_url}
1123
+ Duration: ${result.duration_ms}ms`
1124
+ }
1125
+ ]
1126
+ };
1127
+ }
1128
+ case "kode_production_enable": {
1129
+ const { productionDomain } = args;
1130
+ const result = await client.setProductionEnabled(siteId, true, productionDomain);
1131
+ let text = "Production environment enabled!\n\n";
1132
+ if (result.productionDomain) {
1133
+ text += `Domain: ${result.productionDomain}
1134
+ `;
1135
+ }
1136
+ text += "\nNext steps:\n";
1137
+ text += "1. Deploy to staging: kode_deploy\n";
1138
+ text += "2. Promote to production: kode_promote";
1139
+ return {
1140
+ content: [{ type: "text", text }]
1141
+ };
1142
+ }
1143
+ case "kode_production_disable": {
1144
+ await client.setProductionEnabled(siteId, false);
1145
+ return {
1146
+ content: [
1147
+ {
1148
+ type: "text",
1149
+ text: "Production environment disabled.\n\nOnly staging is now active. Production domain requests will receive an empty script with a warning."
1150
+ }
1151
+ ]
1152
+ };
1153
+ }
790
1154
  case "kode_status": {
791
1155
  const status = await client.getDeploymentStatus(siteId);
792
1156
  const config = getConfig();
793
1157
  let text = `Site: ${config?.siteName || "Unknown"}
794
1158
 
795
1159
  `;
1160
+ const productionEnabled = status.productionEnabled ?? false;
1161
+ text += `PRODUCTION STATUS: ${productionEnabled ? "\u2713 Enabled" : "\u25CB Disabled (staging only)"}
1162
+ `;
1163
+ if (productionEnabled && status.productionDomain) {
1164
+ text += ` Domain: ${status.productionDomain}
1165
+ `;
1166
+ }
1167
+ text += "\n";
796
1168
  text += `STAGING:
797
1169
  `;
798
1170
  if (status.staging.lastSuccessful) {
@@ -807,17 +1179,25 @@ Status: ${deployment.status}`
807
1179
  text += `
808
1180
  PRODUCTION:
809
1181
  `;
810
- if (status.production.lastSuccessful) {
1182
+ if (!productionEnabled) {
1183
+ text += ` (Disabled - use kode_production_enable to activate)
1184
+ `;
1185
+ } else if (status.production.lastSuccessful) {
811
1186
  text += ` Version: ${status.production.lastSuccessful.version}
812
1187
  `;
813
1188
  text += ` Deployed: ${status.production.lastSuccessful.completed_at}
814
1189
  `;
815
1190
  } else {
816
- text += ` Not deployed
1191
+ text += ` Enabled, not yet deployed
817
1192
  `;
818
1193
  }
819
- text += `
1194
+ if (productionEnabled) {
1195
+ text += `
820
1196
  Can promote staging to production: ${status.canPromote ? "Yes" : "No"}`;
1197
+ } else {
1198
+ text += `
1199
+ Enable production first (kode_production_enable) before promoting.`;
1200
+ }
821
1201
  return {
822
1202
  content: [{ type: "text", text }]
823
1203
  };
@@ -1002,6 +1382,184 @@ Embed code:
1002
1382
  content: [{ type: "text", text }]
1003
1383
  };
1004
1384
  }
1385
+ case "kode_analyze_script": {
1386
+ const { slug, saveToScript } = args;
1387
+ const scripts = await client.listScripts(siteId);
1388
+ const script = scripts.find((s) => s.slug === slug || s.id === slug);
1389
+ if (!script) {
1390
+ return {
1391
+ content: [{ type: "text", text: `Script "${slug}" not found` }],
1392
+ isError: true
1393
+ };
1394
+ }
1395
+ try {
1396
+ const result = await client.analyzeScript(script.id, { saveToScript });
1397
+ let text = `Script Analysis: ${script.name}
1398
+
1399
+ `;
1400
+ text += `Purpose: ${result.metadata.purpose || "(not detected)"}
1401
+
1402
+ `;
1403
+ if (result.metadata.triggers.length > 0) {
1404
+ text += `Triggers:
1405
+ `;
1406
+ for (const t of result.metadata.triggers) {
1407
+ text += ` - ${t.event}${t.selector ? ` on ${t.selector}` : ""}
1408
+ `;
1409
+ }
1410
+ text += "\n";
1411
+ }
1412
+ if (result.metadata.domTargets.length > 0) {
1413
+ text += `DOM Targets:
1414
+ `;
1415
+ for (const d of result.metadata.domTargets) {
1416
+ text += ` - ${d.selector} (${d.action})
1417
+ `;
1418
+ }
1419
+ text += "\n";
1420
+ }
1421
+ if (result.metadata.dependencies.length > 0) {
1422
+ text += `Dependencies:
1423
+ `;
1424
+ for (const dep of result.metadata.dependencies) {
1425
+ text += ` - ${dep.name}${dep.required ? " (required)" : ""}
1426
+ `;
1427
+ }
1428
+ text += "\n";
1429
+ }
1430
+ text += `Flags:
1431
+ `;
1432
+ text += ` - Modifies DOM: ${result.metadata.modifiesDom ? "Yes" : "No"}
1433
+ `;
1434
+ text += ` - Event listeners: ${result.metadata.addsEventListeners ? "Yes" : "No"}
1435
+ `;
1436
+ if (result.metadata.usesExternalApis) text += ` - Uses external APIs: Yes
1437
+ `;
1438
+ if (result.metadata.usesLocalStorage) text += ` - Uses localStorage: Yes
1439
+ `;
1440
+ if (result.metadata.usesCookies) text += ` - Uses cookies: Yes
1441
+ `;
1442
+ if (result.warnings.length > 0) {
1443
+ text += `
1444
+ Warnings:
1445
+ `;
1446
+ for (const w of result.warnings) {
1447
+ text += ` \u26A0\uFE0F ${w}
1448
+ `;
1449
+ }
1450
+ }
1451
+ if (result.suggestions.length > 0) {
1452
+ text += `
1453
+ Suggestions:
1454
+ `;
1455
+ for (const s of result.suggestions) {
1456
+ text += ` \u{1F4A1} ${s}
1457
+ `;
1458
+ }
1459
+ }
1460
+ if (saveToScript) {
1461
+ text += `
1462
+ \u2713 Metadata saved to script. Run kode_deploy to update CDN.`;
1463
+ } else {
1464
+ text += `
1465
+ Use saveToScript: true to save this metadata to the script.`;
1466
+ }
1467
+ return {
1468
+ content: [{ type: "text", text }]
1469
+ };
1470
+ } catch (error) {
1471
+ const message = error instanceof Error ? error.message : "Unknown error";
1472
+ return {
1473
+ content: [{ type: "text", text: `Failed to analyze script: ${message}` }],
1474
+ isError: true
1475
+ };
1476
+ }
1477
+ }
1478
+ case "kode_get_script_metadata": {
1479
+ const { slug } = args;
1480
+ const scripts = await client.listScripts(siteId);
1481
+ const script = scripts.find((s) => s.slug === slug || s.id === slug);
1482
+ if (!script) {
1483
+ return {
1484
+ content: [{ type: "text", text: `Script "${slug}" not found` }],
1485
+ isError: true
1486
+ };
1487
+ }
1488
+ try {
1489
+ const result = await client.getScriptMetadata(script.id);
1490
+ if (!result.metadata) {
1491
+ return {
1492
+ content: [{
1493
+ type: "text",
1494
+ text: `Script "${script.name}" has no metadata.
1495
+
1496
+ Use kode_analyze_script to generate metadata, or provide metadata when creating/updating the script.`
1497
+ }]
1498
+ };
1499
+ }
1500
+ let text = `Metadata for: ${script.name}
1501
+
1502
+ `;
1503
+ if (result.metadata.purpose) {
1504
+ text += `Purpose: ${result.metadata.purpose}
1505
+
1506
+ `;
1507
+ }
1508
+ if (result.aiSummary) {
1509
+ text += `AI Summary: ${result.aiSummary}
1510
+
1511
+ `;
1512
+ }
1513
+ if (result.metadata.triggers.length > 0) {
1514
+ text += `Triggers:
1515
+ `;
1516
+ for (const t of result.metadata.triggers) {
1517
+ text += ` - ${t.event}${t.selector ? ` on ${t.selector}` : ""}
1518
+ `;
1519
+ }
1520
+ text += "\n";
1521
+ }
1522
+ if (result.metadata.domTargets.length > 0) {
1523
+ text += `DOM Targets:
1524
+ `;
1525
+ for (const d of result.metadata.domTargets) {
1526
+ text += ` - ${d.selector} (${d.action})
1527
+ `;
1528
+ }
1529
+ text += "\n";
1530
+ }
1531
+ if (result.metadata.dependencies.length > 0) {
1532
+ text += `Dependencies:
1533
+ `;
1534
+ for (const dep of result.metadata.dependencies) {
1535
+ text += ` - ${dep.name}${dep.required ? " (required)" : ""}
1536
+ `;
1537
+ }
1538
+ text += "\n";
1539
+ }
1540
+ text += `Flags:
1541
+ `;
1542
+ text += ` - AI Generated: ${result.aiGenerated ? "Yes" : "No"}
1543
+ `;
1544
+ text += ` - Modifies DOM: ${result.metadata.modifiesDom ? "Yes" : "No"}
1545
+ `;
1546
+ text += ` - Event listeners: ${result.metadata.addsEventListeners ? "Yes" : "No"}
1547
+ `;
1548
+ if (result.lastAnalyzed) {
1549
+ text += `
1550
+ Last analyzed: ${result.lastAnalyzed}`;
1551
+ }
1552
+ return {
1553
+ content: [{ type: "text", text }]
1554
+ };
1555
+ } catch (error) {
1556
+ const message = error instanceof Error ? error.message : "Unknown error";
1557
+ return {
1558
+ content: [{ type: "text", text: `Failed to get metadata: ${message}` }],
1559
+ isError: true
1560
+ };
1561
+ }
1562
+ }
1005
1563
  case "kode_read_context": {
1006
1564
  const contextPath = getContextPath();
1007
1565
  if (!contextPath) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@curenorway/kode-mcp",
3
- "version": "1.2.0",
4
- "description": "MCP server for Cure Kode - enables AI agents to manage Webflow scripts",
3
+ "version": "1.4.0",
4
+ "description": "MCP server for Cure Kode CDN - enables AI agents to manage, deploy, and analyze Webflow scripts",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cure-kode-mcp": "./dist/index.js"