@curenorway/kode-mcp 1.0.0 → 1.0.1

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 +32 -4
  2. package/dist/index.js +719 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -55,7 +55,7 @@ Run `kode init` in your project to create `.cure-kode/config.json`:
55
55
  ```bash
56
56
  export CURE_KODE_API_KEY="ck_..."
57
57
  export CURE_KODE_SITE_ID="your-site-uuid"
58
- export CURE_KODE_API_URL="https://cure-app-v2-production.up.railway.app" # optional
58
+ export CURE_KODE_API_URL="https://app.cure.no" # optional
59
59
  ```
60
60
 
61
61
  ## Available Tools
@@ -109,10 +109,38 @@ Once configured, you can ask Claude:
109
109
 
110
110
  ## Security
111
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
112
+ ### API Key Authentication
113
+
114
+ - **SHA256 Hashed Storage**: API keys are hashed before storage on the server
115
+ - **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
121
+ - **Expiration**: Keys can have optional expiration dates
122
+
123
+ ### Local Security
124
+
125
+ - API keys stored in `.cure-kode/config.json` (auto-gitignored)
115
126
  - Never commit API keys to version control
127
+ - Keys prefixed with `ck_` for easy identification
128
+
129
+ ### Network Security
130
+
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
116
144
 
117
145
  ## Troubleshooting
118
146
 
package/dist/index.js CHANGED
@@ -115,7 +115,7 @@ var KodeApiClient = class {
115
115
  // src/config.ts
116
116
  import * as fs from "fs";
117
117
  import * as path from "path";
118
- var DEFAULT_API_URL = "https://cure-app-v2-production.up.railway.app";
118
+ var DEFAULT_API_URL = "https://app.cure.no";
119
119
  var CONFIG_DIR = ".cure-kode";
120
120
  var CONFIG_FILE = "config.json";
121
121
  function findProjectRoot(startDir = process.cwd()) {
@@ -171,10 +171,30 @@ function getScriptsDir() {
171
171
  if (!projectRoot || !projectConfig) return void 0;
172
172
  return path.join(projectRoot, projectConfig.scriptsDir);
173
173
  }
174
+ function getContextPath() {
175
+ const projectRoot = findProjectRoot();
176
+ if (!projectRoot) return void 0;
177
+ return path.join(projectRoot, CONFIG_DIR, "context.md");
178
+ }
179
+ function getPagesDir() {
180
+ const projectRoot = findProjectRoot();
181
+ if (!projectRoot) return void 0;
182
+ return path.join(projectRoot, CONFIG_DIR, "pages");
183
+ }
174
184
 
175
185
  // src/index.ts
176
186
  import * as fs2 from "fs";
177
187
  import * as path2 from "path";
188
+ function urlToSlug(url) {
189
+ try {
190
+ const parsed = new URL(url);
191
+ let slug = parsed.pathname.replace(/^\//, "").replace(/\//g, "--") || "home";
192
+ slug = slug.replace(/--$/, "");
193
+ return slug.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
194
+ } catch {
195
+ return "page";
196
+ }
197
+ }
178
198
  var server = new Server(
179
199
  {
180
200
  name: "cure-kode",
@@ -365,6 +385,123 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
365
385
  properties: {},
366
386
  required: []
367
387
  }
388
+ },
389
+ {
390
+ name: "kode_read_context",
391
+ 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.",
392
+ inputSchema: {
393
+ type: "object",
394
+ properties: {},
395
+ required: []
396
+ }
397
+ },
398
+ {
399
+ name: "kode_update_context",
400
+ description: "Update the project context file. Use this to add notes, record session history, or update script purposes. Call this after making changes to persist your discoveries.",
401
+ inputSchema: {
402
+ type: "object",
403
+ properties: {
404
+ addNote: {
405
+ type: "string",
406
+ description: "Add a note to the notes section (e.g., HTML structure discovery, third-party integration found)"
407
+ },
408
+ addSession: {
409
+ type: "object",
410
+ description: "Record a work session",
411
+ properties: {
412
+ task: {
413
+ type: "string",
414
+ description: "What task was performed"
415
+ },
416
+ changes: {
417
+ type: "array",
418
+ items: { type: "string" },
419
+ description: "List of changes made"
420
+ }
421
+ },
422
+ required: ["task", "changes"]
423
+ },
424
+ updateScriptPurpose: {
425
+ type: "object",
426
+ description: "Update a script's purpose description",
427
+ properties: {
428
+ slug: {
429
+ type: "string",
430
+ description: "Script slug"
431
+ },
432
+ purpose: {
433
+ type: "string",
434
+ description: "Purpose description"
435
+ }
436
+ },
437
+ required: ["slug", "purpose"]
438
+ }
439
+ },
440
+ required: []
441
+ }
442
+ },
443
+ {
444
+ name: "kode_fetch_html_smart",
445
+ description: "Fetch and analyze a webpage with smart CMS truncation. Shows template pattern instead of all CMS items. Better for understanding page structure.",
446
+ inputSchema: {
447
+ type: "object",
448
+ properties: {
449
+ url: {
450
+ type: "string",
451
+ description: 'Full URL to analyze (e.g., "https://example.com")'
452
+ },
453
+ truncateCms: {
454
+ type: "boolean",
455
+ description: "Truncate CMS collections to show template pattern only. Default: true"
456
+ },
457
+ includeHtml: {
458
+ type: "boolean",
459
+ description: "Include HTML preview in response. Default: false (returns structure only)"
460
+ }
461
+ },
462
+ required: ["url"]
463
+ }
464
+ },
465
+ {
466
+ name: "kode_refresh_page",
467
+ description: 'Fetch webpage and extract FULL structure with CSS selectors. WHEN TO USE: (1) User says "HTML changed" or "refresh", (2) Cache is >7 days old, (3) First time analyzing a page, (4) Before writing code that targets elements. Returns: sections, CTAs, forms, CMS, headings, scripts, images, embeds, interactions - all with CSS selectors for development.',
468
+ inputSchema: {
469
+ type: "object",
470
+ properties: {
471
+ url: {
472
+ type: "string",
473
+ description: 'Full URL to analyze and cache (e.g., "https://example.com/about")'
474
+ },
475
+ force: {
476
+ type: "boolean",
477
+ description: "Force refresh even if cached. Use when user says HTML has changed."
478
+ }
479
+ },
480
+ required: ["url"]
481
+ }
482
+ },
483
+ {
484
+ name: "kode_get_page_context",
485
+ description: "Get FULL page context with CSS selectors for development. Returns: sections with selectors, navigation, CTAs with classes, forms with field selectors, CMS patterns, headings, scripts inventory, images, embeds, and Webflow interactions. Warns if cache is >7 days old. Use kode_refresh_page if HTML has changed or cache is stale.",
486
+ inputSchema: {
487
+ type: "object",
488
+ properties: {
489
+ urlOrSlug: {
490
+ type: "string",
491
+ description: 'URL or slug of the cached page (e.g., "https://example.com/about" or "about")'
492
+ }
493
+ },
494
+ required: ["urlOrSlug"]
495
+ }
496
+ },
497
+ {
498
+ name: "kode_list_pages_context",
499
+ description: "List all cached page contexts. Shows which pages have been analyzed and when. Use kode_refresh_page to add new pages.",
500
+ inputSchema: {
501
+ type: "object",
502
+ properties: {},
503
+ required: []
504
+ }
368
505
  }
369
506
  ]
370
507
  };
@@ -665,6 +802,587 @@ Embed code:
665
802
  content: [{ type: "text", text }]
666
803
  };
667
804
  }
805
+ case "kode_read_context": {
806
+ const contextPath = getContextPath();
807
+ if (!contextPath) {
808
+ return {
809
+ content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
810
+ isError: true
811
+ };
812
+ }
813
+ if (!fs2.existsSync(contextPath)) {
814
+ return {
815
+ content: [{ type: "text", text: 'No context file found. Run "kode context --refresh" to create one.' }],
816
+ isError: true
817
+ };
818
+ }
819
+ const content = fs2.readFileSync(contextPath, "utf-8");
820
+ return {
821
+ content: [{ type: "text", text: content }]
822
+ };
823
+ }
824
+ case "kode_update_context": {
825
+ const contextPath = getContextPath();
826
+ if (!contextPath) {
827
+ return {
828
+ content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
829
+ isError: true
830
+ };
831
+ }
832
+ if (!fs2.existsSync(contextPath)) {
833
+ return {
834
+ content: [{ type: "text", text: 'No context file found. Run "kode context --refresh" to create one.' }],
835
+ isError: true
836
+ };
837
+ }
838
+ const { addNote, addSession, updateScriptPurpose } = args;
839
+ let content = fs2.readFileSync(contextPath, "utf-8");
840
+ const updates = [];
841
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
842
+ content = content.replace(
843
+ /> Last updated: .+$/m,
844
+ `> Last updated: ${timestamp}`
845
+ );
846
+ content = content.replace(
847
+ /> Updated by: .+$/m,
848
+ "> Updated by: Claude (AI Agent)"
849
+ );
850
+ if (addNote) {
851
+ const notesMatch = content.match(/## Notes\n\n((?:- .+\n?)*)/m);
852
+ if (notesMatch) {
853
+ const existingNotes = notesMatch[1];
854
+ const cleanedNotes = existingNotes.split("\n").filter((line) => line && !line.includes("(HTML structure") && !line.includes("(Third-party") && !line.includes("(Known issues")).join("\n");
855
+ const newNotes = cleanedNotes ? `${cleanedNotes}
856
+ - ${addNote}
857
+ ` : `- ${addNote}
858
+ `;
859
+ content = content.replace(
860
+ /## Notes\n\n(?:- .+\n?)*/m,
861
+ `## Notes
862
+
863
+ ${newNotes}`
864
+ );
865
+ updates.push(`Added note: "${addNote}"`);
866
+ }
867
+ }
868
+ if (addSession) {
869
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
870
+ const sessionMd = `### ${date} - Claude
871
+ **Task**: ${addSession.task}
872
+ **Changes**:
873
+ ${addSession.changes.map((c) => `- ${c}`).join("\n")}
874
+
875
+ `;
876
+ const sessionsMatch = content.match(/## Sessions\n\n/);
877
+ if (sessionsMatch) {
878
+ content = content.replace(
879
+ /## Sessions\n\n/m,
880
+ `## Sessions
881
+
882
+ ${sessionMd}`
883
+ );
884
+ content = content.replace(/\(No sessions recorded yet\)\n?/m, "");
885
+ updates.push(`Added session: "${addSession.task}"`);
886
+ }
887
+ }
888
+ if (updateScriptPurpose) {
889
+ const { slug, purpose } = updateScriptPurpose;
890
+ const rowRegex = new RegExp(`\\| ${slug} \\| (\\w+) \\| ([\\w-]+) \\| [^|]* \\|`, "m");
891
+ const match = content.match(rowRegex);
892
+ if (match) {
893
+ content = content.replace(
894
+ rowRegex,
895
+ `| ${slug} | ${match[1]} | ${match[2]} | ${purpose} |`
896
+ );
897
+ updates.push(`Updated purpose for "${slug}": "${purpose}"`);
898
+ }
899
+ }
900
+ fs2.writeFileSync(contextPath, content, "utf-8");
901
+ return {
902
+ content: [
903
+ {
904
+ type: "text",
905
+ text: updates.length > 0 ? `Context updated:
906
+ ${updates.map((u) => `- ${u}`).join("\n")}` : "No updates applied"
907
+ }
908
+ ]
909
+ };
910
+ }
911
+ case "kode_fetch_html_smart": {
912
+ const { url, truncateCms = true, includeHtml = false } = args;
913
+ const config = getConfig();
914
+ if (!config) {
915
+ return {
916
+ content: [{ type: "text", text: "Cure Kode not configured" }],
917
+ isError: true
918
+ };
919
+ }
920
+ const response = await fetch(`${config.apiUrl}/api/cdn/fetch-html`, {
921
+ method: "POST",
922
+ headers: {
923
+ "Content-Type": "application/json",
924
+ "X-API-Key": config.apiKey
925
+ },
926
+ body: JSON.stringify({ url, truncateCms })
927
+ });
928
+ if (!response.ok) {
929
+ return {
930
+ content: [{ type: "text", text: `Failed to fetch HTML: ${response.statusText}` }],
931
+ isError: true
932
+ };
933
+ }
934
+ const result = await response.json();
935
+ let text = `URL: ${result.url}
936
+ Title: ${result.title || "(no title)"}
937
+
938
+ `;
939
+ text += `Scripts (${result.stats.totalScripts} total):
940
+ `;
941
+ if (result.scripts.webflow.length > 0) {
942
+ text += ` Webflow: ${result.scripts.webflow.length}
943
+ `;
944
+ }
945
+ if (result.scripts.cureKode.length > 0) {
946
+ text += ` Cure Kode: ${result.scripts.cureKode.length}
947
+ `;
948
+ }
949
+ if (result.scripts.thirdParty.length > 0) {
950
+ text += ` Third-party: ${result.scripts.thirdParty.length}
951
+ `;
952
+ for (const script of result.scripts.thirdParty.slice(0, 5)) {
953
+ text += ` - ${script.type || script.src || "(inline)"}
954
+ `;
955
+ }
956
+ if (result.scripts.thirdParty.length > 5) {
957
+ text += ` ... and ${result.scripts.thirdParty.length - 5} more
958
+ `;
959
+ }
960
+ }
961
+ if (result.scripts.custom.length > 0) {
962
+ text += ` Custom: ${result.scripts.custom.length}
963
+ `;
964
+ }
965
+ text += "\n";
966
+ text += `Styles: ${result.stats.totalStyles}
967
+
968
+ `;
969
+ if (result.detectedComponents.length > 0) {
970
+ text += `Webflow Components:
971
+ `;
972
+ for (const comp of result.detectedComponents) {
973
+ text += ` - ${comp}
974
+ `;
975
+ }
976
+ text += "\n";
977
+ }
978
+ if (result.cmsCollections && result.cmsCollections.length > 0) {
979
+ text += `CMS Collections (truncated):
980
+ `;
981
+ for (const collection of result.cmsCollections) {
982
+ text += ` - ${collection.selector}: ${collection.itemCount} items
983
+ `;
984
+ }
985
+ text += "\n";
986
+ }
987
+ if (includeHtml && result.htmlPreview) {
988
+ text += `HTML Preview (first 5000 chars):
989
+ ${result.htmlPreview.slice(0, 5e3)}`;
990
+ if (result.htmlPreview.length > 5e3) {
991
+ text += "\n... (truncated)";
992
+ }
993
+ }
994
+ return {
995
+ content: [{ type: "text", text }]
996
+ };
997
+ }
998
+ case "kode_refresh_page": {
999
+ const { url, force = false } = args;
1000
+ const pagesDir = getPagesDir();
1001
+ if (!pagesDir) {
1002
+ return {
1003
+ content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
1004
+ isError: true
1005
+ };
1006
+ }
1007
+ const slug = urlToSlug(url);
1008
+ const cachePath = path2.join(pagesDir, `${slug}.json`);
1009
+ if (!force && fs2.existsSync(cachePath)) {
1010
+ try {
1011
+ const cached = JSON.parse(fs2.readFileSync(cachePath, "utf-8"));
1012
+ return {
1013
+ content: [{
1014
+ type: "text",
1015
+ text: `Using cached version from ${cached.extractedAt}
1016
+ Slug: ${slug}
1017
+ Use force: true to refresh.
1018
+
1019
+ Page: ${cached.title || url}
1020
+ Sections: ${cached.sections.length}
1021
+ CTAs: ${cached.ctas.length}
1022
+ Forms: ${cached.forms.length}
1023
+ CMS Collections: ${cached.cmsPatterns.length}`
1024
+ }]
1025
+ };
1026
+ } catch {
1027
+ }
1028
+ }
1029
+ const config = getConfig();
1030
+ if (!config) {
1031
+ return {
1032
+ content: [{ type: "text", text: "Cure Kode not configured" }],
1033
+ isError: true
1034
+ };
1035
+ }
1036
+ const response = await fetch(`${config.apiUrl}/api/cdn/page-context`, {
1037
+ method: "POST",
1038
+ headers: {
1039
+ "Content-Type": "application/json",
1040
+ "X-API-Key": config.apiKey
1041
+ },
1042
+ body: JSON.stringify({ url })
1043
+ });
1044
+ if (!response.ok) {
1045
+ return {
1046
+ content: [{ type: "text", text: `Failed to fetch page: ${response.statusText}` }],
1047
+ isError: true
1048
+ };
1049
+ }
1050
+ const structure = await response.json();
1051
+ if (!fs2.existsSync(pagesDir)) {
1052
+ fs2.mkdirSync(pagesDir, { recursive: true });
1053
+ }
1054
+ fs2.writeFileSync(cachePath, JSON.stringify(structure, null, 2), "utf-8");
1055
+ const contextPath = getContextPath();
1056
+ if (contextPath && fs2.existsSync(contextPath)) {
1057
+ }
1058
+ let text = `Page cached: ${slug}
1059
+ Path: ${cachePath}
1060
+
1061
+ `;
1062
+ text += `Title: ${structure.title || "(no title)"}
1063
+ `;
1064
+ text += `URL: ${structure.url}
1065
+
1066
+ `;
1067
+ text += `Sections (${structure.sections.length}):
1068
+ `;
1069
+ for (const s of structure.sections.slice(0, 6)) {
1070
+ const name2 = s.heading || s.id || s.className?.split(" ")[0] || "section";
1071
+ const flags = [s.hasCms && "CMS", s.hasForm && "Form"].filter(Boolean).join(", ");
1072
+ text += ` - ${name2}${flags ? ` [${flags}]` : ""}
1073
+ `;
1074
+ }
1075
+ if (structure.sections.length > 6) {
1076
+ text += ` ... and ${structure.sections.length - 6} more
1077
+ `;
1078
+ }
1079
+ if (structure.ctas.length > 0) {
1080
+ text += `
1081
+ CTAs (${structure.ctas.length}):
1082
+ `;
1083
+ for (const c of structure.ctas.slice(0, 4)) {
1084
+ text += ` - "${c.text}"
1085
+ `;
1086
+ }
1087
+ if (structure.ctas.length > 4) {
1088
+ text += ` ... and ${structure.ctas.length - 4} more
1089
+ `;
1090
+ }
1091
+ }
1092
+ if (structure.forms.length > 0) {
1093
+ text += `
1094
+ Forms (${structure.forms.length}):
1095
+ `;
1096
+ for (const f of structure.forms) {
1097
+ text += ` - ${f.name || "form"}: ${f.fields.length} fields
1098
+ `;
1099
+ }
1100
+ }
1101
+ if (structure.cmsPatterns.length > 0) {
1102
+ text += `
1103
+ CMS Collections (${structure.cmsPatterns.length}):
1104
+ `;
1105
+ for (const c of structure.cmsPatterns) {
1106
+ text += ` - ${c.containerSelector}: ${c.itemCount} items
1107
+ `;
1108
+ }
1109
+ }
1110
+ return {
1111
+ content: [{ type: "text", text }]
1112
+ };
1113
+ }
1114
+ case "kode_get_page_context": {
1115
+ const { urlOrSlug } = args;
1116
+ const pagesDir = getPagesDir();
1117
+ if (!pagesDir) {
1118
+ return {
1119
+ content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
1120
+ isError: true
1121
+ };
1122
+ }
1123
+ const slug = urlOrSlug.startsWith("http") ? urlToSlug(urlOrSlug) : urlOrSlug;
1124
+ const cachePath = path2.join(pagesDir, `${slug}.json`);
1125
+ if (!fs2.existsSync(cachePath)) {
1126
+ return {
1127
+ content: [{
1128
+ type: "text",
1129
+ text: `Page not found: ${slug}
1130
+
1131
+ Use kode_list_pages_context to see cached pages, or kode_refresh_page to cache a new page.`
1132
+ }],
1133
+ isError: true
1134
+ };
1135
+ }
1136
+ const context = JSON.parse(fs2.readFileSync(cachePath, "utf-8"));
1137
+ const cacheAge = Date.now() - new Date(context.extractedAt).getTime();
1138
+ const cacheAgeDays = Math.floor(cacheAge / (1e3 * 60 * 60 * 24));
1139
+ const isStale = cacheAgeDays > 7;
1140
+ let text = `# ${context.title || context.url}
1141
+
1142
+ `;
1143
+ text += `URL: ${context.url}
1144
+ `;
1145
+ text += `Cached: ${context.extractedAt}${isStale ? ` \u26A0\uFE0F (${cacheAgeDays} days old - consider refreshing with kode_refresh_page)` : ""}
1146
+ `;
1147
+ if (context.stats) {
1148
+ text += `Stats: ${context.stats.sectionCount} sections, ${context.stats.ctaCount} CTAs, ${context.stats.formCount} forms, ${context.stats.imageCount} images, ${context.stats.scriptCount} scripts, ${context.stats.interactionCount} interactions
1149
+ `;
1150
+ }
1151
+ text += "\n";
1152
+ if (context.sections && context.sections.length > 0) {
1153
+ text += `## Sections (${context.sections.length})
1154
+
1155
+ `;
1156
+ for (const s of context.sections) {
1157
+ const flags = [s.hasCms && "CMS", s.hasForm && "Form", s.webflowInteraction && "Interaction"].filter(Boolean).join(", ");
1158
+ text += `### ${s.heading || s.selector}
1159
+ `;
1160
+ text += `Selector: \`${s.selector}\`
1161
+ `;
1162
+ if (flags) text += `Flags: [${flags}]
1163
+ `;
1164
+ if (s.textSample) text += `Preview: ${s.textSample.slice(0, 120)}...
1165
+ `;
1166
+ text += "\n";
1167
+ }
1168
+ }
1169
+ if (context.navigation && context.navigation.length > 0) {
1170
+ text += `## Navigation
1171
+
1172
+ `;
1173
+ for (const n of context.navigation) {
1174
+ text += `### ${n.type} nav
1175
+ `;
1176
+ text += `Selector: \`${n.selector}\`
1177
+ `;
1178
+ for (const item of n.items.slice(0, 10)) {
1179
+ text += ` - "${item.text}" \`${item.selector}\`${item.href ? ` \u2192 ${item.href}` : ""}
1180
+ `;
1181
+ }
1182
+ text += "\n";
1183
+ }
1184
+ }
1185
+ if (context.ctas && context.ctas.length > 0) {
1186
+ text += `## CTAs (${context.ctas.length})
1187
+
1188
+ `;
1189
+ for (const c of context.ctas) {
1190
+ text += `- "${c.text}" \`${c.selector}\`${c.href ? ` \u2192 ${c.href}` : ""}
1191
+ `;
1192
+ if (c.classes && c.classes.length > 0) {
1193
+ text += ` Classes: ${c.classes.join(", ")}
1194
+ `;
1195
+ }
1196
+ }
1197
+ text += "\n";
1198
+ }
1199
+ if (context.forms && context.forms.length > 0) {
1200
+ text += `## Forms (${context.forms.length})
1201
+
1202
+ `;
1203
+ for (const f of context.forms) {
1204
+ text += `### ${f.name || f.id || "Form"}
1205
+ `;
1206
+ text += `Selector: \`${f.selector}\`
1207
+ `;
1208
+ if (f.action) text += `Action: ${f.action} (${f.method || "GET"})
1209
+ `;
1210
+ text += `Fields:
1211
+ `;
1212
+ for (const field of f.fields) {
1213
+ const req = field.required ? " *" : "";
1214
+ text += ` - ${field.label || field.name || field.type}${req}: \`${field.selector}\`
1215
+ `;
1216
+ }
1217
+ if (f.submitButton) {
1218
+ text += `Submit: "${f.submitButton.text}" \`${f.submitButton.selector}\`
1219
+ `;
1220
+ }
1221
+ text += "\n";
1222
+ }
1223
+ }
1224
+ if (context.cmsPatterns && context.cmsPatterns.length > 0) {
1225
+ text += `## CMS Collections (${context.cmsPatterns.length})
1226
+
1227
+ `;
1228
+ for (const c of context.cmsPatterns) {
1229
+ text += `Container: \`${c.containerSelector}\` (${c.itemCount} items)
1230
+ `;
1231
+ text += `Item: \`${c.itemSelector}\`
1232
+ `;
1233
+ if (c.templateFields && c.templateFields.length > 0) {
1234
+ text += `Template fields: ${c.templateFields.join(", ")}
1235
+ `;
1236
+ }
1237
+ text += "\n";
1238
+ }
1239
+ }
1240
+ if (context.headings && context.headings.length > 0) {
1241
+ text += `## Headings (${context.headings.length})
1242
+
1243
+ `;
1244
+ for (const h of context.headings.slice(0, 15)) {
1245
+ const indent = " ".repeat(h.level - 1);
1246
+ text += `${indent}H${h.level}: "${h.text}" \`${h.selector}\`
1247
+ `;
1248
+ }
1249
+ if (context.headings.length > 15) {
1250
+ text += `... and ${context.headings.length - 15} more
1251
+ `;
1252
+ }
1253
+ text += "\n";
1254
+ }
1255
+ if (context.scripts && context.scripts.length > 0) {
1256
+ text += `## Scripts (${context.scripts.length})
1257
+
1258
+ `;
1259
+ const byCategory = {};
1260
+ for (const s of context.scripts) {
1261
+ byCategory[s.category] = byCategory[s.category] || [];
1262
+ byCategory[s.category].push(s);
1263
+ }
1264
+ for (const [cat, scripts] of Object.entries(byCategory)) {
1265
+ text += `### ${cat} (${scripts.length})
1266
+ `;
1267
+ for (const s of scripts.slice(0, 5)) {
1268
+ if (s.src) {
1269
+ text += ` - ${s.src}${s.async ? " (async)" : ""}${s.defer ? " (defer)" : ""}
1270
+ `;
1271
+ } else {
1272
+ text += ` - [inline ${s.type}] ${s.contentPreview?.slice(0, 50)}...
1273
+ `;
1274
+ }
1275
+ }
1276
+ if (scripts.length > 5) text += ` ... and ${scripts.length - 5} more
1277
+ `;
1278
+ }
1279
+ text += "\n";
1280
+ }
1281
+ if (context.images && context.images.length > 0) {
1282
+ text += `## Images (${context.images.length})
1283
+
1284
+ `;
1285
+ for (const img of context.images.slice(0, 10)) {
1286
+ const dims = img.dimensions ? ` (${img.dimensions.width}\xD7${img.dimensions.height})` : "";
1287
+ text += `- "${img.alt || "no alt"}" \`${img.selector}\`${dims}
1288
+ `;
1289
+ }
1290
+ if (context.images.length > 10) {
1291
+ text += `... and ${context.images.length - 10} more
1292
+ `;
1293
+ }
1294
+ text += "\n";
1295
+ }
1296
+ if (context.webflowInteractions && context.webflowInteractions.length > 0) {
1297
+ text += `## Webflow Interactions (${context.webflowInteractions.length})
1298
+
1299
+ `;
1300
+ for (const i of context.webflowInteractions) {
1301
+ text += `- \`${i.triggerSelector}\`${i.type ? ` (${i.type})` : ""}
1302
+ `;
1303
+ }
1304
+ text += "\n";
1305
+ }
1306
+ if (context.embeds && context.embeds.length > 0) {
1307
+ text += `## Custom Embeds (${context.embeds.length})
1308
+
1309
+ `;
1310
+ for (const e of context.embeds) {
1311
+ text += `- ${e.type} embed \`${e.selector}\` in ${e.location}
1312
+ `;
1313
+ }
1314
+ text += "\n";
1315
+ }
1316
+ if (context.notes && context.notes.length > 0) {
1317
+ text += `## Notes
1318
+
1319
+ `;
1320
+ for (const note of context.notes) {
1321
+ text += `- ${note}
1322
+ `;
1323
+ }
1324
+ }
1325
+ return {
1326
+ content: [{ type: "text", text }]
1327
+ };
1328
+ }
1329
+ case "kode_list_pages_context": {
1330
+ const pagesDir = getPagesDir();
1331
+ if (!pagesDir) {
1332
+ return {
1333
+ content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
1334
+ isError: true
1335
+ };
1336
+ }
1337
+ if (!fs2.existsSync(pagesDir)) {
1338
+ return {
1339
+ content: [{
1340
+ type: "text",
1341
+ text: "No cached pages.\n\nUse kode_refresh_page to analyze and cache a page's structure."
1342
+ }]
1343
+ };
1344
+ }
1345
+ const files = fs2.readdirSync(pagesDir).filter((f) => f.endsWith(".json"));
1346
+ if (files.length === 0) {
1347
+ return {
1348
+ content: [{
1349
+ type: "text",
1350
+ text: "No cached pages.\n\nUse kode_refresh_page to analyze and cache a page's structure."
1351
+ }]
1352
+ };
1353
+ }
1354
+ let text = `Cached Pages (${files.length})
1355
+
1356
+ `;
1357
+ for (const file of files) {
1358
+ const slug = file.replace(".json", "");
1359
+ try {
1360
+ const context = JSON.parse(fs2.readFileSync(path2.join(pagesDir, file), "utf-8"));
1361
+ const urlPath = new URL(context.url).pathname;
1362
+ text += `${urlPath} [${slug}]
1363
+ `;
1364
+ if (context.title) {
1365
+ text += ` "${context.title}"
1366
+ `;
1367
+ }
1368
+ text += ` Sections: ${context.sections.length}, CTAs: ${context.ctas.length}, Forms: ${context.forms.length}, CMS: ${context.cmsPatterns.length}
1369
+ `;
1370
+ text += ` Cached: ${context.extractedAt}
1371
+
1372
+ `;
1373
+ } catch {
1374
+ text += `${slug} (invalid cache file)
1375
+
1376
+ `;
1377
+ }
1378
+ }
1379
+ text += `Use kode_get_page_context to see full details for a page.
1380
+ `;
1381
+ text += `Use kode_refresh_page to update or add pages.`;
1382
+ return {
1383
+ content: [{ type: "text", text }]
1384
+ };
1385
+ }
668
1386
  default:
669
1387
  return {
670
1388
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curenorway/kode-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "MCP server for Cure Kode - enables AI agents to manage Webflow scripts",
5
5
  "type": "module",
6
6
  "bin": {