@decantr/mcp-server 1.0.0-beta.11 → 1.0.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/bin.js +1 -1
- package/dist/{chunk-ARK7P76H.js → chunk-YOFQYTEC.js} +73 -32
- package/dist/critique-DKY4NB2O.js +74 -0
- package/dist/index.js +1 -1
- package/package.json +10 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Decantr AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/bin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-YOFQYTEC.js";
|
|
@@ -1091,6 +1091,20 @@ var TOOLS = [
|
|
|
1091
1091
|
required: ["component"]
|
|
1092
1092
|
},
|
|
1093
1093
|
annotations: READ_ONLY
|
|
1094
|
+
},
|
|
1095
|
+
// 15. decantr_critique — local read
|
|
1096
|
+
{
|
|
1097
|
+
name: "decantr_critique",
|
|
1098
|
+
title: "Design Critique",
|
|
1099
|
+
description: "Evaluate generated code against the essence spec for visual quality. Returns a scorecard covering treatment usage, decorator coverage, personality alignment, motion, accessibility, and responsiveness.",
|
|
1100
|
+
inputSchema: {
|
|
1101
|
+
type: "object",
|
|
1102
|
+
properties: {
|
|
1103
|
+
file_path: { type: "string", description: "Path to the component file to critique" }
|
|
1104
|
+
},
|
|
1105
|
+
required: ["file_path"]
|
|
1106
|
+
},
|
|
1107
|
+
annotations: READ_ONLY
|
|
1094
1108
|
}
|
|
1095
1109
|
];
|
|
1096
1110
|
async function handleTool(name, args) {
|
|
@@ -1198,14 +1212,14 @@ async function handleTool(name, args) {
|
|
|
1198
1212
|
try {
|
|
1199
1213
|
const blueprint = await apiClient.getBlueprint(namespace, args.id);
|
|
1200
1214
|
let topology = null;
|
|
1201
|
-
const composeEntries = blueprint.compose
|
|
1215
|
+
const composeEntries = blueprint.compose;
|
|
1202
1216
|
if (composeEntries && Array.isArray(composeEntries) && composeEntries.length > 0) {
|
|
1203
1217
|
const zoneInputs = [];
|
|
1204
1218
|
const archetypePromises = composeEntries.map(async (entry) => {
|
|
1205
1219
|
const arcId = typeof entry === "string" ? entry : entry.archetype;
|
|
1206
1220
|
try {
|
|
1207
1221
|
const arch = await apiClient.getContent("archetypes", namespace, arcId);
|
|
1208
|
-
const archData = arch
|
|
1222
|
+
const archData = arch;
|
|
1209
1223
|
const explicitRole = typeof entry === "object" ? entry.role : void 0;
|
|
1210
1224
|
zoneInputs.push({
|
|
1211
1225
|
archetypeId: arcId,
|
|
@@ -1252,50 +1266,72 @@ async function handleTool(name, args) {
|
|
|
1252
1266
|
namespace: "@official",
|
|
1253
1267
|
limit: 100
|
|
1254
1268
|
});
|
|
1255
|
-
const
|
|
1269
|
+
const prelimScores = [];
|
|
1256
1270
|
for (const p of patternsResponse.items) {
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
...p.tags || []
|
|
1262
|
-
].join(" ").toLowerCase();
|
|
1271
|
+
const slug = p.slug || "";
|
|
1272
|
+
const name2 = p.name || slug;
|
|
1273
|
+
const description = p.description || "";
|
|
1274
|
+
const searchable = [name2, description].join(" ").toLowerCase();
|
|
1263
1275
|
let score = 0;
|
|
1264
1276
|
const words = desc.split(/\s+/);
|
|
1265
1277
|
for (const word of words) {
|
|
1266
1278
|
if (word.length < 3) continue;
|
|
1267
1279
|
if (searchable.includes(word)) score += 10;
|
|
1268
1280
|
}
|
|
1269
|
-
if (desc.includes("dashboard") && ["kpi-grid", "chart-grid", "data-table", "filter-bar"].includes(
|
|
1270
|
-
if (desc.includes("metric") &&
|
|
1271
|
-
if (desc.includes("chart") &&
|
|
1272
|
-
if (desc.includes("table") &&
|
|
1273
|
-
if (desc.includes("form") &&
|
|
1274
|
-
if (desc.includes("setting") &&
|
|
1275
|
-
if (desc.includes("landing") && ["hero", "cta-section", "card-grid"].includes(
|
|
1276
|
-
if (desc.includes("hero") &&
|
|
1277
|
-
if (desc.includes("ecommerce") && ["card-grid", "filter-bar", "detail-header"].includes(
|
|
1278
|
-
if (desc.includes("product") &&
|
|
1279
|
-
if (desc.includes("feed") &&
|
|
1280
|
-
if (desc.includes("filter") &&
|
|
1281
|
-
if (desc.includes("search") &&
|
|
1281
|
+
if (desc.includes("dashboard") && ["kpi-grid", "chart-grid", "data-table", "filter-bar"].includes(slug)) score += 20;
|
|
1282
|
+
if (desc.includes("metric") && slug === "kpi-grid") score += 15;
|
|
1283
|
+
if (desc.includes("chart") && slug === "chart-grid") score += 15;
|
|
1284
|
+
if (desc.includes("table") && slug === "data-table") score += 15;
|
|
1285
|
+
if (desc.includes("form") && slug === "form-sections") score += 15;
|
|
1286
|
+
if (desc.includes("setting") && slug === "form-sections") score += 15;
|
|
1287
|
+
if (desc.includes("landing") && ["hero", "cta-section", "card-grid"].includes(slug)) score += 20;
|
|
1288
|
+
if (desc.includes("hero") && slug === "hero") score += 20;
|
|
1289
|
+
if (desc.includes("ecommerce") && ["card-grid", "filter-bar", "detail-header"].includes(slug)) score += 15;
|
|
1290
|
+
if (desc.includes("product") && slug === "card-grid") score += 15;
|
|
1291
|
+
if (desc.includes("feed") && slug === "activity-feed") score += 15;
|
|
1292
|
+
if (desc.includes("filter") && slug === "filter-bar") score += 15;
|
|
1293
|
+
if (desc.includes("search") && slug === "filter-bar") score += 10;
|
|
1282
1294
|
if (score > 0) {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1295
|
+
prelimScores.push({ slug, score, name: name2, description });
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
prelimScores.sort((a, b) => b.score - a.score);
|
|
1299
|
+
const top10 = prelimScores.slice(0, 10);
|
|
1300
|
+
const suggestions = [];
|
|
1301
|
+
for (const candidate of top10) {
|
|
1302
|
+
let fullPattern = null;
|
|
1303
|
+
try {
|
|
1304
|
+
const fetched = await apiClient.getPattern("@official", candidate.slug);
|
|
1305
|
+
fullPattern = fetched;
|
|
1306
|
+
} catch {
|
|
1292
1307
|
}
|
|
1308
|
+
let score = candidate.score;
|
|
1309
|
+
if (fullPattern) {
|
|
1310
|
+
const fullSearchable = [
|
|
1311
|
+
...fullPattern.components || [],
|
|
1312
|
+
...fullPattern.tags || []
|
|
1313
|
+
].join(" ").toLowerCase();
|
|
1314
|
+
const words = desc.split(/\s+/);
|
|
1315
|
+
for (const word of words) {
|
|
1316
|
+
if (word.length < 3) continue;
|
|
1317
|
+
if (fullSearchable.includes(word)) score += 10;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
const preset = fullPattern?.presets ? Object.values(fullPattern.presets)[0] : null;
|
|
1321
|
+
suggestions.push({
|
|
1322
|
+
id: candidate.slug,
|
|
1323
|
+
score,
|
|
1324
|
+
name: fullPattern?.name || candidate.name,
|
|
1325
|
+
description: fullPattern?.description || candidate.description,
|
|
1326
|
+
components: fullPattern?.components || [],
|
|
1327
|
+
layout: preset?.layout ? preset.layout.layout : "grid"
|
|
1328
|
+
});
|
|
1293
1329
|
}
|
|
1294
1330
|
suggestions.sort((a, b) => b.score - a.score);
|
|
1295
1331
|
return {
|
|
1296
1332
|
query: args.description,
|
|
1297
1333
|
suggestions: suggestions.slice(0, 5),
|
|
1298
|
-
total:
|
|
1334
|
+
total: prelimScores.length
|
|
1299
1335
|
};
|
|
1300
1336
|
} catch (e) {
|
|
1301
1337
|
return { error: `Could not fetch patterns: ${e.message}` };
|
|
@@ -1746,6 +1782,11 @@ async function handleTool(name, args) {
|
|
|
1746
1782
|
}
|
|
1747
1783
|
return { content: lines.join("\n") };
|
|
1748
1784
|
}
|
|
1785
|
+
case "decantr_critique": {
|
|
1786
|
+
const { critiqueFile } = await import("./critique-DKY4NB2O.js");
|
|
1787
|
+
const result = await critiqueFile(args.file_path, process.cwd());
|
|
1788
|
+
return result;
|
|
1789
|
+
}
|
|
1749
1790
|
default:
|
|
1750
1791
|
return { error: `Unknown tool: ${name}` };
|
|
1751
1792
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/critique.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
async function critiqueFile(filePath, projectRoot) {
|
|
5
|
+
const code = await readFile(filePath, "utf-8");
|
|
6
|
+
const codeLower = code.toLowerCase();
|
|
7
|
+
let treatments = "";
|
|
8
|
+
try {
|
|
9
|
+
treatments = await readFile(join(projectRoot, "src", "styles", "treatments.css"), "utf-8");
|
|
10
|
+
} catch {
|
|
11
|
+
}
|
|
12
|
+
const scores = [];
|
|
13
|
+
const treatmentClasses = ["d-interactive", "d-surface", "d-data", "d-control", "d-section", "d-annotation", "d-label"];
|
|
14
|
+
const usedTreatments = treatmentClasses.filter((t) => code.includes(t));
|
|
15
|
+
scores.push({
|
|
16
|
+
category: "Treatment Usage",
|
|
17
|
+
score: Math.min(5, Math.max(1, Math.round(usedTreatments.length / treatmentClasses.length * 5))),
|
|
18
|
+
details: `${usedTreatments.length}/${treatmentClasses.length} base treatments used: ${usedTreatments.join(", ") || "none"}`,
|
|
19
|
+
suggestions: treatmentClasses.filter((t) => !code.includes(t)).map((t) => `Consider using \`${t}\` where appropriate`)
|
|
20
|
+
});
|
|
21
|
+
const decoratorMatches = treatments.match(/\.([\w-]+)\s*\{/g) || [];
|
|
22
|
+
const decoratorNames = decoratorMatches.map((m) => m.slice(1).replace(/\s*\{$/, "")).filter((n) => !n.startsWith("d-") && !["neon-glow", "neon-glow-hover", "neon-text-glow", "neon-border-glow", "mono-data", "status-ring", "entrance-fade"].includes(n));
|
|
23
|
+
const usedDecorators = decoratorNames.filter((d) => code.includes(d));
|
|
24
|
+
const decTarget = Math.min(decoratorNames.length, 5);
|
|
25
|
+
scores.push({
|
|
26
|
+
category: "Decorator Usage",
|
|
27
|
+
score: decTarget > 0 ? Math.min(5, Math.max(1, Math.round(usedDecorators.length / decTarget * 5))) : 3,
|
|
28
|
+
details: `${usedDecorators.length}/${decoratorNames.length} theme decorators used`,
|
|
29
|
+
suggestions: decoratorNames.filter((d) => !code.includes(d)).slice(0, 3).map((d) => `Theme decorator \`${d}\` available but unused`)
|
|
30
|
+
});
|
|
31
|
+
const personalityUtils = ["neon-glow", "neon-text-glow", "neon-border-glow", "mono-data", "status-ring", "entrance-fade"];
|
|
32
|
+
const availableUtils = personalityUtils.filter((u) => treatments.includes(u));
|
|
33
|
+
const usedUtils = availableUtils.filter((u) => code.includes(u));
|
|
34
|
+
const utilTarget = Math.min(availableUtils.length, 3);
|
|
35
|
+
scores.push({
|
|
36
|
+
category: "Personality Alignment",
|
|
37
|
+
score: utilTarget > 0 ? Math.min(5, Math.max(1, Math.round(usedUtils.length / utilTarget * 5))) : 3,
|
|
38
|
+
details: `${usedUtils.length}/${availableUtils.length} personality utilities used`,
|
|
39
|
+
suggestions: availableUtils.filter((u) => !code.includes(u)).map((u) => `Personality utility \`${u}\` defined but not used`)
|
|
40
|
+
});
|
|
41
|
+
const hasTransition = codeLower.includes("transition") || codeLower.includes("animate") || codeLower.includes("keyframe") || codeLower.includes("framer");
|
|
42
|
+
const hasHover = codeLower.includes(":hover") || codeLower.includes("onmouseenter") || codeLower.includes("hover:");
|
|
43
|
+
scores.push({
|
|
44
|
+
category: "Motion & Interaction",
|
|
45
|
+
score: Math.min(5, (hasTransition ? 3 : 1) + (hasHover ? 1 : 0)),
|
|
46
|
+
details: `Transitions: ${hasTransition ? "yes" : "no"}, Hover states: ${hasHover ? "yes" : "no"}`,
|
|
47
|
+
suggestions: hasTransition ? [] : ["Add transition effects for interactive elements"]
|
|
48
|
+
});
|
|
49
|
+
const hasAria = codeLower.includes("aria-") || codeLower.includes("role=");
|
|
50
|
+
const hasFocus = codeLower.includes("focus-visible") || codeLower.includes("focusvisible");
|
|
51
|
+
const hasKeyboard = codeLower.includes("onkeydown") || codeLower.includes("onkeyup");
|
|
52
|
+
scores.push({
|
|
53
|
+
category: "Accessibility",
|
|
54
|
+
score: Math.min(5, 1 + (hasAria ? 2 : 0) + (hasFocus ? 1 : 0) + (hasKeyboard ? 1 : 0)),
|
|
55
|
+
details: `ARIA: ${hasAria}, Focus: ${hasFocus}, Keyboard: ${hasKeyboard}`,
|
|
56
|
+
suggestions: [
|
|
57
|
+
...!hasAria ? ["Add ARIA attributes to interactive regions"] : [],
|
|
58
|
+
...!hasKeyboard ? ["Add keyboard event handlers"] : []
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
const hasMedia = codeLower.includes("@media") || codeLower.includes("usemediaquery") || codeLower.includes("breakpoint");
|
|
62
|
+
const hasResponsive = codeLower.includes("sm:") || codeLower.includes("md:") || codeLower.includes("lg:") || codeLower.includes("_sm_") || codeLower.includes("_md_");
|
|
63
|
+
scores.push({
|
|
64
|
+
category: "Responsive Design",
|
|
65
|
+
score: Math.min(5, (hasMedia ? 3 : 1) + (hasResponsive ? 2 : 0)),
|
|
66
|
+
details: `Media queries: ${hasMedia}, Responsive classes: ${hasResponsive}`,
|
|
67
|
+
suggestions: !hasMedia && !hasResponsive ? ["Add responsive breakpoint handling"] : []
|
|
68
|
+
});
|
|
69
|
+
const overall = Math.round(scores.reduce((s, c) => s + c.score, 0) / scores.length * 10) / 10;
|
|
70
|
+
return { file: filePath, overall, scores };
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
critiqueFile
|
|
74
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-YOFQYTEC.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decantr/mcp-server",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.12",
|
|
4
4
|
"description": "MCP server for Decantr — exposes design intelligence tools to AI coding assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,18 +15,20 @@
|
|
|
15
15
|
},
|
|
16
16
|
"main": "dist/index.js",
|
|
17
17
|
"types": "dist/index.d.ts",
|
|
18
|
-
"files": [
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
19
21
|
"publishConfig": {
|
|
20
22
|
"access": "public"
|
|
21
23
|
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
26
|
+
"@decantr/essence-spec": "1.0.0-beta.11",
|
|
27
|
+
"@decantr/registry": "1.0.0-beta.11"
|
|
28
|
+
},
|
|
22
29
|
"scripts": {
|
|
23
30
|
"build": "tsup",
|
|
24
31
|
"test": "vitest run",
|
|
25
32
|
"test:watch": "vitest"
|
|
26
|
-
},
|
|
27
|
-
"dependencies": {
|
|
28
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
29
|
-
"@decantr/essence-spec": "workspace:*",
|
|
30
|
-
"@decantr/registry": "workspace:*"
|
|
31
33
|
}
|
|
32
|
-
}
|
|
34
|
+
}
|