@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 +152 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +730 -0
- package/package.json +46 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|