@primer/mcp 0.3.4 → 0.4.0-rc.2d1806208
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/dist/index.d.ts +6 -0
- package/dist/index.js +2 -19
- package/dist/server-BTJ9W5jN.js +1190 -0
- package/dist/stdio.js +5 -21
- package/dist/transports/stdio.d.ts +1 -0
- package/package.json +6 -15
- package/src/primer.ts +33 -0
- package/src/server.ts +24 -11
- package/dist/server-C2QaEv-c.js +0 -1504
- package/dist/server-CGPYmiEJ.js +0 -1504
- package/dist/server-CaSuVJt0.js +0 -1561
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import * as cheerio from "cheerio";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import TurndownService from "turndown";
|
|
6
|
+
import componentsMetadata from "@primer/react/generated/components.json" with { type: "json" };
|
|
7
|
+
import octicons from "@primer/octicons/build/data.json" with { type: "json" };
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import baseMotion from "@primer/primitives/dist/docs/base/motion/motion.json" with { type: "json" };
|
|
11
|
+
import baseSize from "@primer/primitives/dist/docs/base/size/size.json" with { type: "json" };
|
|
12
|
+
import baseTypography from "@primer/primitives/dist/docs/base/typography/typography.json" with { type: "json" };
|
|
13
|
+
import functionalSizeBorder from "@primer/primitives/dist/docs/functional/size/border.json" with { type: "json" };
|
|
14
|
+
import functionalSizeCoarse from "@primer/primitives/dist/docs/functional/size/size-coarse.json" with { type: "json" };
|
|
15
|
+
import functionalSizeFine from "@primer/primitives/dist/docs/functional/size/size-fine.json" with { type: "json" };
|
|
16
|
+
import functionalSize from "@primer/primitives/dist/docs/functional/size/size.json" with { type: "json" };
|
|
17
|
+
import light from "@primer/primitives/dist/docs/functional/themes/light.json" with { type: "json" };
|
|
18
|
+
import functionalTypography from "@primer/primitives/dist/docs/functional/typography/typography.json" with { type: "json" };
|
|
19
|
+
//#region src/primer.ts
|
|
20
|
+
function idToSlug(id) {
|
|
21
|
+
if (id === "actionbar") return "action-bar";
|
|
22
|
+
if (id === "tooltip-v2") return "tooltip";
|
|
23
|
+
if (id === "dialog_v2") return "dialog";
|
|
24
|
+
return id.replaceAll("_", "-");
|
|
25
|
+
}
|
|
26
|
+
const components = Object.entries(componentsMetadata.components).map(([id, component]) => {
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
name: component.name,
|
|
30
|
+
importPath: component.importPath,
|
|
31
|
+
slug: idToSlug(id)
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
function listComponents() {
|
|
35
|
+
return components;
|
|
36
|
+
}
|
|
37
|
+
const patterns = [
|
|
38
|
+
{
|
|
39
|
+
id: "copy",
|
|
40
|
+
name: "Copy",
|
|
41
|
+
category: "scenario"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "delete",
|
|
45
|
+
name: "Delete",
|
|
46
|
+
category: "scenario"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "filter",
|
|
50
|
+
name: "Filter",
|
|
51
|
+
category: "scenario"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "search",
|
|
55
|
+
name: "Search",
|
|
56
|
+
category: "scenario"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "data-visualization",
|
|
60
|
+
name: "Data Visualization",
|
|
61
|
+
category: "ui"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "degraded-experiences",
|
|
65
|
+
name: "Degraded Experiences",
|
|
66
|
+
category: "ui"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "empty-states",
|
|
70
|
+
name: "Empty States",
|
|
71
|
+
category: "ui"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "feature-onboarding",
|
|
75
|
+
name: "Feature Onboarding",
|
|
76
|
+
category: "ui"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "forms",
|
|
80
|
+
name: "Forms",
|
|
81
|
+
category: "ui"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "loading",
|
|
85
|
+
name: "Loading",
|
|
86
|
+
category: "ui"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "navigation",
|
|
90
|
+
name: "Navigation",
|
|
91
|
+
category: "ui"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "notification-messaging",
|
|
95
|
+
name: "Notification message",
|
|
96
|
+
category: "ui"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "progressive-disclosure",
|
|
100
|
+
name: "Progressive disclosure",
|
|
101
|
+
category: "ui"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "saving",
|
|
105
|
+
name: "Saving",
|
|
106
|
+
category: "ui"
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
function listPatterns() {
|
|
110
|
+
return patterns;
|
|
111
|
+
}
|
|
112
|
+
const icons = Object.values(octicons).map((icon) => {
|
|
113
|
+
return {
|
|
114
|
+
name: icon.name,
|
|
115
|
+
keywords: icon.keywords,
|
|
116
|
+
heights: Object.keys(icon.heights)
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
function listIcons() {
|
|
120
|
+
return icons;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/primitives.ts
|
|
124
|
+
let functionalSizeRadius = {};
|
|
125
|
+
try {
|
|
126
|
+
const radiusPath = createRequire(import.meta.url).resolve("@primer/primitives/dist/docs/functional/size/radius.json");
|
|
127
|
+
functionalSizeRadius = JSON.parse(readFileSync(radiusPath, "utf-8"));
|
|
128
|
+
} catch {}
|
|
129
|
+
const categories = {
|
|
130
|
+
base: {
|
|
131
|
+
motion: Object.values(baseMotion).map((token) => {
|
|
132
|
+
return {
|
|
133
|
+
name: token.name,
|
|
134
|
+
type: token.type,
|
|
135
|
+
value: token.value
|
|
136
|
+
};
|
|
137
|
+
}),
|
|
138
|
+
size: Object.values(baseSize).map((token) => {
|
|
139
|
+
return {
|
|
140
|
+
name: token.name,
|
|
141
|
+
type: token.type,
|
|
142
|
+
value: token.value
|
|
143
|
+
};
|
|
144
|
+
}),
|
|
145
|
+
typography: Object.values(baseTypography).map((token) => {
|
|
146
|
+
return {
|
|
147
|
+
name: token.name,
|
|
148
|
+
type: token.type,
|
|
149
|
+
value: token.value
|
|
150
|
+
};
|
|
151
|
+
})
|
|
152
|
+
},
|
|
153
|
+
functional: {
|
|
154
|
+
border: Object.values(functionalSizeBorder).map((token) => {
|
|
155
|
+
return {
|
|
156
|
+
name: token.name,
|
|
157
|
+
type: token.type,
|
|
158
|
+
value: token.value
|
|
159
|
+
};
|
|
160
|
+
}),
|
|
161
|
+
radius: Object.values(functionalSizeRadius).map((token) => {
|
|
162
|
+
return {
|
|
163
|
+
name: token.name,
|
|
164
|
+
type: token.type,
|
|
165
|
+
value: token.value
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
168
|
+
sizeCoarse: Object.values(functionalSizeCoarse).map((token) => {
|
|
169
|
+
return {
|
|
170
|
+
name: token.name,
|
|
171
|
+
type: token.type,
|
|
172
|
+
value: token.value
|
|
173
|
+
};
|
|
174
|
+
}),
|
|
175
|
+
sizeFine: Object.values(functionalSizeFine).map((token) => {
|
|
176
|
+
return {
|
|
177
|
+
name: token.name,
|
|
178
|
+
type: token.type,
|
|
179
|
+
value: token.value
|
|
180
|
+
};
|
|
181
|
+
}),
|
|
182
|
+
size: Object.values(functionalSize).map((token) => {
|
|
183
|
+
return {
|
|
184
|
+
name: token.name,
|
|
185
|
+
type: token.type,
|
|
186
|
+
value: token.value
|
|
187
|
+
};
|
|
188
|
+
}),
|
|
189
|
+
themes: { light: Object.values(light).map((token) => {
|
|
190
|
+
return {
|
|
191
|
+
name: token.name,
|
|
192
|
+
type: token.type,
|
|
193
|
+
value: token.value
|
|
194
|
+
};
|
|
195
|
+
}) },
|
|
196
|
+
typography: Object.values(functionalTypography).map((token) => {
|
|
197
|
+
return {
|
|
198
|
+
name: token.name,
|
|
199
|
+
type: token.type,
|
|
200
|
+
value: token.value
|
|
201
|
+
};
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const tokens = [
|
|
206
|
+
...categories.base.motion,
|
|
207
|
+
...categories.base.size,
|
|
208
|
+
...categories.base.typography,
|
|
209
|
+
...categories.functional.border,
|
|
210
|
+
...categories.functional.radius,
|
|
211
|
+
...categories.functional.sizeCoarse,
|
|
212
|
+
...categories.functional.sizeFine,
|
|
213
|
+
...categories.functional.size,
|
|
214
|
+
...categories.functional.themes.light,
|
|
215
|
+
...categories.functional.typography
|
|
216
|
+
];
|
|
217
|
+
const SEMANTIC_PREFIXES = [
|
|
218
|
+
"bgColor",
|
|
219
|
+
"fgColor",
|
|
220
|
+
"border",
|
|
221
|
+
"borderColor",
|
|
222
|
+
"shadow",
|
|
223
|
+
"focus",
|
|
224
|
+
"color",
|
|
225
|
+
"animation",
|
|
226
|
+
"duration"
|
|
227
|
+
];
|
|
228
|
+
function listTokenGroups() {
|
|
229
|
+
const allTokens = tokens;
|
|
230
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const token of allTokens) {
|
|
232
|
+
const parts = token.name.split("-");
|
|
233
|
+
const prefix = parts[0];
|
|
234
|
+
if (!groupMap.has(prefix)) groupMap.set(prefix, {
|
|
235
|
+
count: 0,
|
|
236
|
+
subGroups: /* @__PURE__ */ new Set()
|
|
237
|
+
});
|
|
238
|
+
const group = groupMap.get(prefix);
|
|
239
|
+
group.count++;
|
|
240
|
+
if (!SEMANTIC_PREFIXES.includes(prefix) && parts.length > 1) {
|
|
241
|
+
const subGroup = parts[1];
|
|
242
|
+
if (SEMANTIC_PREFIXES.includes(subGroup)) group.subGroups.add(subGroup);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const semantic = [];
|
|
246
|
+
const component = [];
|
|
247
|
+
for (const [name, data] of groupMap.entries()) {
|
|
248
|
+
const group = {
|
|
249
|
+
name,
|
|
250
|
+
count: data.count
|
|
251
|
+
};
|
|
252
|
+
if (data.subGroups.size > 0) group.subGroups = Array.from(data.subGroups).sort();
|
|
253
|
+
if (SEMANTIC_PREFIXES.includes(name)) semantic.push(group);
|
|
254
|
+
else component.push(group);
|
|
255
|
+
}
|
|
256
|
+
semantic.sort((a, b) => b.count - a.count);
|
|
257
|
+
component.sort((a, b) => b.count - a.count);
|
|
258
|
+
return {
|
|
259
|
+
semantic,
|
|
260
|
+
component
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function parseDesignTokensSpec(markdown) {
|
|
264
|
+
const results = [];
|
|
265
|
+
const lines = markdown.split("\n");
|
|
266
|
+
let currentGroup = "";
|
|
267
|
+
let currentToken = null;
|
|
268
|
+
let descriptionLines = [];
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
const groupMatch = line.match(/^## (.+)$/);
|
|
271
|
+
if (groupMatch) {
|
|
272
|
+
if (currentToken?.name) {
|
|
273
|
+
results.push({
|
|
274
|
+
name: currentToken.name,
|
|
275
|
+
value: getTokenValue(currentToken.name),
|
|
276
|
+
useCase: currentToken.useCase || descriptionLines.join(" "),
|
|
277
|
+
rules: currentToken.rules || "",
|
|
278
|
+
group: currentToken.group || ""
|
|
279
|
+
});
|
|
280
|
+
descriptionLines = [];
|
|
281
|
+
}
|
|
282
|
+
currentGroup = groupMatch[1].trim();
|
|
283
|
+
currentToken = null;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const tokenMatch = line.match(/^### (.+)$/);
|
|
287
|
+
if (tokenMatch) {
|
|
288
|
+
if (currentToken?.name) results.push({
|
|
289
|
+
name: currentToken.name,
|
|
290
|
+
value: getTokenValue(currentToken.name),
|
|
291
|
+
useCase: currentToken.useCase || descriptionLines.join(" "),
|
|
292
|
+
rules: currentToken.rules || "",
|
|
293
|
+
group: currentToken.group || ""
|
|
294
|
+
});
|
|
295
|
+
descriptionLines = [];
|
|
296
|
+
currentToken = {
|
|
297
|
+
name: tokenMatch[1].trim(),
|
|
298
|
+
group: currentGroup
|
|
299
|
+
};
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const newUsageMatch = line.match(/^\*\*U:\*\*\s*(.+)$/);
|
|
303
|
+
if (newUsageMatch && currentToken) {
|
|
304
|
+
currentToken.useCase = newUsageMatch[1].trim();
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const newRulesMatch = line.match(/^\*\*R:\*\*\s*(.+)$/);
|
|
308
|
+
if (newRulesMatch && currentToken) {
|
|
309
|
+
currentToken.rules = newRulesMatch[1].trim();
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (currentToken && !currentToken.useCase && !line.startsWith("**") && line.trim() && !line.startsWith("#")) descriptionLines.push(line.trim());
|
|
313
|
+
}
|
|
314
|
+
if (currentToken?.name) results.push({
|
|
315
|
+
name: currentToken.name,
|
|
316
|
+
value: getTokenValue(currentToken.name),
|
|
317
|
+
useCase: currentToken.useCase || descriptionLines.join(" "),
|
|
318
|
+
rules: currentToken.rules || "",
|
|
319
|
+
group: currentToken.group || ""
|
|
320
|
+
});
|
|
321
|
+
return results;
|
|
322
|
+
}
|
|
323
|
+
function getTokenValue(tokenName) {
|
|
324
|
+
const found = tokens.find((token) => token.name === tokenName);
|
|
325
|
+
return found ? String(found.value) : "";
|
|
326
|
+
}
|
|
327
|
+
const GROUP_LABELS = {
|
|
328
|
+
bgColor: "Background Color",
|
|
329
|
+
fgColor: "Foreground Color",
|
|
330
|
+
borderColor: "Border Color",
|
|
331
|
+
border: "Border",
|
|
332
|
+
shadow: "Shadow",
|
|
333
|
+
focus: "Focus",
|
|
334
|
+
color: "Color",
|
|
335
|
+
borderWidth: "Border Width",
|
|
336
|
+
borderRadius: "Border Radius",
|
|
337
|
+
boxShadow: "Box Shadow",
|
|
338
|
+
controlStack: "Control Stack",
|
|
339
|
+
fontStack: "Font Stack",
|
|
340
|
+
outline: "Outline",
|
|
341
|
+
text: "Text",
|
|
342
|
+
control: "Control",
|
|
343
|
+
overlay: "Overlay",
|
|
344
|
+
stack: "Stack",
|
|
345
|
+
spinner: "Spinner"
|
|
346
|
+
};
|
|
347
|
+
function getGroupFromName(name) {
|
|
348
|
+
return name.split("-")[0];
|
|
349
|
+
}
|
|
350
|
+
function buildAllTokens(guidelinesTokens) {
|
|
351
|
+
const guidelinesMap = new Map(guidelinesTokens.map((t) => [t.name, t]));
|
|
352
|
+
const allSourceTokens = [
|
|
353
|
+
...categories.base.motion,
|
|
354
|
+
...categories.base.size,
|
|
355
|
+
...categories.base.typography,
|
|
356
|
+
...categories.functional.themes.light,
|
|
357
|
+
...categories.functional.size,
|
|
358
|
+
...categories.functional.sizeCoarse,
|
|
359
|
+
...categories.functional.sizeFine,
|
|
360
|
+
...categories.functional.border,
|
|
361
|
+
...categories.functional.radius,
|
|
362
|
+
...categories.functional.typography
|
|
363
|
+
];
|
|
364
|
+
const allTokens = [];
|
|
365
|
+
const seen = /* @__PURE__ */ new Set();
|
|
366
|
+
for (const token of allSourceTokens) {
|
|
367
|
+
if (seen.has(token.name)) continue;
|
|
368
|
+
seen.add(token.name);
|
|
369
|
+
const existing = guidelinesMap.get(token.name);
|
|
370
|
+
if (existing) allTokens.push(existing);
|
|
371
|
+
else allTokens.push({
|
|
372
|
+
name: token.name,
|
|
373
|
+
value: String(token.value),
|
|
374
|
+
useCase: "",
|
|
375
|
+
rules: "",
|
|
376
|
+
group: getGroupFromName(token.name)
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return allTokens;
|
|
380
|
+
}
|
|
381
|
+
function expandTokenPattern(token) {
|
|
382
|
+
const bracketRegex = /\[(.*?)\]/;
|
|
383
|
+
const match = token.name.match(bracketRegex);
|
|
384
|
+
if (!match) return [token];
|
|
385
|
+
return match[1].split(",").map((s) => s.trim()).map((variant) => ({
|
|
386
|
+
...token,
|
|
387
|
+
name: token.name.replace(bracketRegex, variant)
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
function loadAllTokensWithGuidelines() {
|
|
391
|
+
try {
|
|
392
|
+
return buildAllTokens(parseDesignTokensSpec(loadDesignTokensSpec()));
|
|
393
|
+
} catch {
|
|
394
|
+
return buildAllTokens([]);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function loadDesignTokensGuide() {
|
|
398
|
+
return readFileSync(createRequire(import.meta.url).resolve("@primer/primitives/DESIGN_TOKENS_GUIDE.md"), "utf-8");
|
|
399
|
+
}
|
|
400
|
+
function loadDesignTokensSpec() {
|
|
401
|
+
return readFileSync(createRequire(import.meta.url).resolve("@primer/primitives/DESIGN_TOKENS_SPEC.md"), "utf-8");
|
|
402
|
+
}
|
|
403
|
+
function getDesignTokenSpecsText(groups) {
|
|
404
|
+
return `
|
|
405
|
+
# Design Token Specifications
|
|
406
|
+
|
|
407
|
+
## 1. Core Rule & Enforcement
|
|
408
|
+
* **Expert Mode**: CSS expert. NEVER use raw values (hex, px, etc.). Tokens only.
|
|
409
|
+
* **Motion & Transitions:** Every interactive state change (Hover, Active) MUST include a transition. NEVER use raw values like 200ms or ease-in. Use var(--base-duration-...) and var(--base-easing-...).
|
|
410
|
+
* **Shorthand**: MUST use \`font: var(...)\`. NEVER split size/weight.
|
|
411
|
+
* **Shorthand Fallback**: If no shorthand exists (e.g. Monospace), use individual tokens for font-size, family, and line-height. NEVER raw 1.5.
|
|
412
|
+
* **States**: Define 5: Rest, Hover, Focus-visible, Active, Disabled.
|
|
413
|
+
* **Focus**: \`:focus-visible\` MUST use \`outline: var(--focus-outline)\` AND \`outline-offset: var(--focus-outline-offset, var(--outline-focus-offset))\`.
|
|
414
|
+
* **Validation**: CALL \`lint_css\` after any CSS change. Task is incomplete without a success message.
|
|
415
|
+
* **Self-Correction**: Adopt autofixes immediately. Report unfixable errors to the user.
|
|
416
|
+
|
|
417
|
+
## 2. Typography Constraints (STRICT)
|
|
418
|
+
- **Body Only**: Only \`body\` group supports size suffixes (e.g., \`body-small\`).
|
|
419
|
+
- **Static Shorthands**: NEVER add suffixes to \`caption\`, \`display\`, \`codeBlock\`, or \`codeInline\`.
|
|
420
|
+
|
|
421
|
+
## 3. Logic Matrix: Color & Semantic Mapping
|
|
422
|
+
| Input Color/Intent | Semantic Role | Background Suffix | Foreground Requirement |
|
|
423
|
+
| :--- | :--- | :--- | :--- |
|
|
424
|
+
| Blue / Interactive | \`accent\` | \`-emphasis\` (Solid) | \`fgColor-onEmphasis\` |
|
|
425
|
+
| Green / Positive | \`success\` | \`-muted\` (Light) | \`fgColor-{semantic}\` |
|
|
426
|
+
| Red / Danger | \`danger\` | \`-emphasis\` | \`fgColor-onEmphasis\` |
|
|
427
|
+
| Yellow / Warning | \`attention\` | \`-muted\` | \`fgColor-attention\` |
|
|
428
|
+
| Orange / Critical | \`severe\` | \`-emphasis\` | \`fgColor-onEmphasis\` |
|
|
429
|
+
| Purple / Done | \`done\` | Any | Match intent |
|
|
430
|
+
| Pink / Sponsors | \`sponsors\` | Any | Match intent |
|
|
431
|
+
| Grey / Neutral | \`default\` | \`bgColor-muted\` | \`fgColor-default\` (Not muted) |
|
|
432
|
+
|
|
433
|
+
## 4. Optimization & Recipes (MANDATORY)
|
|
434
|
+
**Strategy**: STOP property-by-property searching. Use \`get_token_group_bundle\` for these common patterns:
|
|
435
|
+
- **Forms**: \`["control", "focus", "outline", "text", "borderRadius", "stack", "animation"]\`
|
|
436
|
+
- **Modals/Cards**: \`["overlay", "shadow", "outline", "borderRadius", "bgColor", "stack", "animation"]\`
|
|
437
|
+
- **Tables/Lists**: \`["stack", "borderColor", "text", "bgColor", "control"]\`
|
|
438
|
+
- **Nav/Sidebars**: \`["control", "text", "accent", "stack", "focus", "animation"]\`
|
|
439
|
+
- **Status/Badges**: \`["text", "success", "danger", "attention", "severe", "stack"]\`
|
|
440
|
+
|
|
441
|
+
## 5. Available Groups
|
|
442
|
+
- **Semantic**: ${groups.semantic.map((g) => `${g.name}\``).join(", ")}
|
|
443
|
+
- **Components**: ${groups.component.map((g) => `\`${g.name}\``).join(", ")}
|
|
444
|
+
`.trim();
|
|
445
|
+
}
|
|
446
|
+
function getTokenUsagePatternsText() {
|
|
447
|
+
return `
|
|
448
|
+
# Design Token Reference Examples
|
|
449
|
+
|
|
450
|
+
> **CRITICAL FOR AI**: To implement the examples below, DO NOT search for tokens one-by-one.
|
|
451
|
+
> Use \`get_token_group_bundle(groups: ["control", "stack", "focus", "borderRadius"])\` to fetch the required token values in a single call.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## 1. Interaction Pattern: The Primary Button
|
|
456
|
+
*Demonstrates: 5 states, color pairing, typography shorthand, and motion.*
|
|
457
|
+
|
|
458
|
+
\`\`\`css
|
|
459
|
+
.btn-primary {
|
|
460
|
+
/* Logic: Use control tokens for interactive elements */
|
|
461
|
+
background-color: var(--control-bgColor-rest);
|
|
462
|
+
color: var(--fgColor-default);
|
|
463
|
+
font: var(--text-body-shorthand-medium); /* MUST use shorthand */
|
|
464
|
+
|
|
465
|
+
/* Scale: DEFAULT is medium/normal */
|
|
466
|
+
padding-block: var(--control-medium-paddingBlock);
|
|
467
|
+
padding-inline: var(--control-medium-paddingInline-normal);
|
|
468
|
+
border: none;
|
|
469
|
+
border-radius: var(--borderRadius-medium);
|
|
470
|
+
cursor: pointer;
|
|
471
|
+
|
|
472
|
+
/* Motion: MUST be <300ms */
|
|
473
|
+
transition: background-color 150ms ease, transform 100ms ease;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.btn-primary:hover {
|
|
477
|
+
background-color: var(--control-bgColor-hover);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.btn-primary:focus-visible {
|
|
481
|
+
outline: var(--focus-outline);
|
|
482
|
+
outline-offset: var(--focus-outline-offset, var(--outline-focus-offset));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.btn-primary:active {
|
|
486
|
+
background-color: var(--control-bgColor-active);
|
|
487
|
+
transform: scale(0.98);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.btn-primary:disabled {
|
|
491
|
+
/* Logic: MUST pair bgColor-disabled with fgColor-disabled */
|
|
492
|
+
background-color: var(--bgColor-disabled);
|
|
493
|
+
color: var(--fgColor-disabled);
|
|
494
|
+
cursor: not-allowed;
|
|
495
|
+
}
|
|
496
|
+
\`\`\`
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## 2. Layout Pattern: Vertical Stack
|
|
501
|
+
*Demonstrates: Layout spacing rules and matching padding density.*
|
|
502
|
+
|
|
503
|
+
\`\`\`css
|
|
504
|
+
.card-stack {
|
|
505
|
+
display: flex;
|
|
506
|
+
flex-direction: column;
|
|
507
|
+
|
|
508
|
+
/* Logic: Use stack tokens for layout spacing */
|
|
509
|
+
gap: var(--stack-gap-normal);
|
|
510
|
+
padding: var(--stack-padding-normal);
|
|
511
|
+
|
|
512
|
+
background-color: var(--bgColor-default);
|
|
513
|
+
border: 1px solid var(--borderColor-default);
|
|
514
|
+
border-radius: var(--borderRadius-large);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* Logic: Matching padding density to purpose */
|
|
518
|
+
.card-header {
|
|
519
|
+
padding-block-end: var(--stack-gap-condensed);
|
|
520
|
+
border-bottom: 1px solid var(--borderColor-muted);
|
|
521
|
+
}
|
|
522
|
+
\`\`\`
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Implementation Rules for AI:
|
|
527
|
+
1. **Shorthand First**: Always use \`font: var(...)\` rather than splitting size/weight.
|
|
528
|
+
2. **States**: Never implement a button without all 5 states.
|
|
529
|
+
3. **Spacing**: Use \`control-\` tokens for the component itself and \`stack-\` tokens for the container/layout.
|
|
530
|
+
4. **Motion**: Always include the \`prefers-reduced-motion\` media query to set transitions to \`none\`.
|
|
531
|
+
\`\`\`css
|
|
532
|
+
@media (prefers-reduced-motion: reduce) {
|
|
533
|
+
.btn-primary {
|
|
534
|
+
transition: none;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
\`\`\`
|
|
538
|
+
`.trim();
|
|
539
|
+
}
|
|
540
|
+
function searchTokens(allTokens, query, group) {
|
|
541
|
+
const expandedTokens = allTokens.flatMap(expandTokenPattern);
|
|
542
|
+
const keywords = query.toLowerCase().split(/\s+/).filter((k) => k.length > 0);
|
|
543
|
+
return expandedTokens.filter((token) => {
|
|
544
|
+
const searchableText = `${token.name} ${token.useCase} ${token.rules} ${token.group}`.toLowerCase();
|
|
545
|
+
const matchesKeywords = keywords.every((word) => searchableText.includes(word));
|
|
546
|
+
const matchesGroup = !group || tokenMatchesGroup(token, group);
|
|
547
|
+
return matchesKeywords && matchesGroup;
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
const GROUP_ALIASES = {
|
|
551
|
+
bgcolor: "bgColor",
|
|
552
|
+
fgcolor: "fgColor",
|
|
553
|
+
bordercolor: "borderColor",
|
|
554
|
+
border: "border",
|
|
555
|
+
shadow: "shadow",
|
|
556
|
+
focus: "focus",
|
|
557
|
+
color: "color",
|
|
558
|
+
button: "button",
|
|
559
|
+
control: "control",
|
|
560
|
+
overlay: "overlay",
|
|
561
|
+
borderradius: "borderRadius",
|
|
562
|
+
boxshadow: "boxShadow",
|
|
563
|
+
fontstack: "fontStack",
|
|
564
|
+
spinner: "spinner",
|
|
565
|
+
background: "bgColor",
|
|
566
|
+
backgroundcolor: "bgColor",
|
|
567
|
+
bg: "bgColor",
|
|
568
|
+
foreground: "fgColor",
|
|
569
|
+
foregroundcolor: "fgColor",
|
|
570
|
+
textcolor: "fgColor",
|
|
571
|
+
fg: "fgColor",
|
|
572
|
+
radius: "borderRadius",
|
|
573
|
+
rounded: "borderRadius",
|
|
574
|
+
elevation: "overlay",
|
|
575
|
+
depth: "overlay",
|
|
576
|
+
btn: "button",
|
|
577
|
+
typography: "text",
|
|
578
|
+
font: "text",
|
|
579
|
+
text: "text",
|
|
580
|
+
"line-height": "text",
|
|
581
|
+
lineheight: "text",
|
|
582
|
+
leading: "text",
|
|
583
|
+
stack: "stack",
|
|
584
|
+
controlstack: "controlStack",
|
|
585
|
+
padding: "stack",
|
|
586
|
+
margin: "stack",
|
|
587
|
+
gap: "stack",
|
|
588
|
+
spacing: "stack",
|
|
589
|
+
layout: "stack",
|
|
590
|
+
offset: "focus",
|
|
591
|
+
outline: "outline",
|
|
592
|
+
ring: "focus",
|
|
593
|
+
borderwidth: "borderWidth",
|
|
594
|
+
line: "borderColor",
|
|
595
|
+
stroke: "borderColor",
|
|
596
|
+
separator: "borderColor",
|
|
597
|
+
red: "danger",
|
|
598
|
+
green: "success",
|
|
599
|
+
yellow: "attention",
|
|
600
|
+
orange: "severe",
|
|
601
|
+
blue: "accent",
|
|
602
|
+
purple: "done",
|
|
603
|
+
pink: "sponsors",
|
|
604
|
+
grey: "neutral",
|
|
605
|
+
gray: "neutral",
|
|
606
|
+
light: "muted",
|
|
607
|
+
subtle: "muted",
|
|
608
|
+
dark: "emphasis",
|
|
609
|
+
strong: "emphasis",
|
|
610
|
+
intense: "emphasis",
|
|
611
|
+
bold: "emphasis",
|
|
612
|
+
vivid: "emphasis",
|
|
613
|
+
highlight: "emphasis"
|
|
614
|
+
};
|
|
615
|
+
function tokenMatchesGroup(token, resolvedGroup) {
|
|
616
|
+
const rg = resolvedGroup.toLowerCase();
|
|
617
|
+
const tokenPrefix = token.name.split("-")[0].toLowerCase();
|
|
618
|
+
const tokenGroup = token.group.toLowerCase();
|
|
619
|
+
return tokenPrefix === rg || tokenGroup === rg;
|
|
620
|
+
}
|
|
621
|
+
function formatBundle(bundleTokens) {
|
|
622
|
+
const grouped = bundleTokens.reduce((acc, token) => {
|
|
623
|
+
const group = GROUP_LABELS[token.group] || token.group || "Ungrouped";
|
|
624
|
+
if (!acc[group]) acc[group] = [];
|
|
625
|
+
acc[group].push(token);
|
|
626
|
+
return acc;
|
|
627
|
+
}, {});
|
|
628
|
+
return Object.entries(grouped).map(([group, groupTokens]) => {
|
|
629
|
+
return `## ${group}\n\n${groupTokens.map((t) => {
|
|
630
|
+
return `- ${t.value ? `\`${t.name}\` → \`${t.value}\`` : `\`${t.name}\``}\n - **U**: ${t.useCase || "(none)"}\n - **R**: ${t.rules || "(none)"}`;
|
|
631
|
+
}).join("\n")}`;
|
|
632
|
+
}).join("\n\n---\n\n");
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Generates a sorted, unique list of group names from the current token cache.
|
|
636
|
+
* Used for "Healing" error messages and the Design System Search Map.
|
|
637
|
+
*/
|
|
638
|
+
function getValidGroupsList(validTokens) {
|
|
639
|
+
if (validTokens.length === 0) return "No groups available.";
|
|
640
|
+
const uniqueGroups = Array.from(new Set(validTokens.map((t) => t.group)));
|
|
641
|
+
uniqueGroups.sort((a, b) => a.localeCompare(b));
|
|
642
|
+
return uniqueGroups.map((g) => `\`${g}\``).join(", ");
|
|
643
|
+
}
|
|
644
|
+
const groupHints = {
|
|
645
|
+
control: "`control` tokens are for form inputs/checkboxes. For buttons, use the `button` group.",
|
|
646
|
+
button: "`button` tokens are for standard triggers. For form-fields, see the `control` group.",
|
|
647
|
+
text: "STRICT: The following typography groups do NOT support size suffixes (-small, -medium, -large): `caption`, `display`, `codeBlock`, and `codeInline`. STRICT: Use shorthand tokens where possible. If splitting, you MUST fetch line-height tokens (e.g., --text-body-lineHeight-small) instead of using raw numbers.",
|
|
648
|
+
fgColor: "Use `fgColor` for text. For borders, use `borderColor`.",
|
|
649
|
+
borderWidth: "`borderWidth` only has sizing values (thin, thick, thicker). For border *colors*, use the `borderColor` or `border` group.",
|
|
650
|
+
animation: "TRANSITION RULE: Apply duration and easing to the base class, not the :hover state. Standard pairing: `transition: background-color var(--base-duration-200) var(--base-easing-easeInOut);`"
|
|
651
|
+
};
|
|
652
|
+
function runStylelint(css) {
|
|
653
|
+
return new Promise((resolve, reject) => {
|
|
654
|
+
const proc = spawn("npx", [
|
|
655
|
+
"stylelint",
|
|
656
|
+
"--stdin",
|
|
657
|
+
"--fix"
|
|
658
|
+
], {
|
|
659
|
+
stdio: [
|
|
660
|
+
"pipe",
|
|
661
|
+
"pipe",
|
|
662
|
+
"pipe"
|
|
663
|
+
],
|
|
664
|
+
shell: true
|
|
665
|
+
});
|
|
666
|
+
let stdout = "";
|
|
667
|
+
let stderr = "";
|
|
668
|
+
proc.stdout.on("data", (data) => {
|
|
669
|
+
stdout += data.toString();
|
|
670
|
+
});
|
|
671
|
+
proc.stderr.on("data", (data) => {
|
|
672
|
+
stderr += data.toString();
|
|
673
|
+
});
|
|
674
|
+
proc.on("close", (code) => {
|
|
675
|
+
if (code === 0) resolve({
|
|
676
|
+
stdout,
|
|
677
|
+
stderr
|
|
678
|
+
});
|
|
679
|
+
else {
|
|
680
|
+
const error = /* @__PURE__ */ new Error(`Stylelint exited with code ${code}`);
|
|
681
|
+
error.stdout = stdout;
|
|
682
|
+
error.stderr = stderr;
|
|
683
|
+
reject(error);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
proc.on("error", reject);
|
|
687
|
+
proc.stdin.write(css);
|
|
688
|
+
proc.stdin.end();
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/server.ts
|
|
693
|
+
const server = new McpServer({
|
|
694
|
+
name: "Primer",
|
|
695
|
+
version: "0.4.0"
|
|
696
|
+
});
|
|
697
|
+
const turndownService = new TurndownService();
|
|
698
|
+
const allTokensWithGuidelines = loadAllTokensWithGuidelines();
|
|
699
|
+
server.registerTool("init", {
|
|
700
|
+
description: "Setup or create a project that includes Primer React",
|
|
701
|
+
annotations: { readOnlyHint: true }
|
|
702
|
+
}, async () => {
|
|
703
|
+
const url = new URL(`/product/getting-started/react`, "https://primer.style");
|
|
704
|
+
const response = await fetch(url);
|
|
705
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
706
|
+
const html = await response.text();
|
|
707
|
+
if (!html) return { content: [] };
|
|
708
|
+
const source = cheerio.load(html)("main").html();
|
|
709
|
+
if (!source) return { content: [] };
|
|
710
|
+
return { content: [{
|
|
711
|
+
type: "text",
|
|
712
|
+
text: `The getting started documentation for Primer React is included below. It's important that the project:
|
|
713
|
+
|
|
714
|
+
- Is using a tool like Vite, Next.js, etc that supports TypeScript and React. If the project does not have support for that, generate an appropriate project scaffold
|
|
715
|
+
- Installs the latest version of \`@primer/react\` from \`npm\`
|
|
716
|
+
- Correctly adds the \`ThemeProvider\` and \`BaseStyles\` components to the root of the application
|
|
717
|
+
- Includes an import to a theme from \`@primer/primitives\`
|
|
718
|
+
- If the project wants to use icons, also install the \`@primer/octicons-react\` from \`npm\`
|
|
719
|
+
- Add appropriate agent instructions (like for copilot) to the project to prefer using components, tokens, icons, and more from Primer packages
|
|
720
|
+
|
|
721
|
+
---
|
|
722
|
+
|
|
723
|
+
${turndownService.turndown(source)}
|
|
724
|
+
`
|
|
725
|
+
}] };
|
|
726
|
+
});
|
|
727
|
+
server.registerTool("list_components", {
|
|
728
|
+
description: "List all of the components available from Primer React",
|
|
729
|
+
annotations: { readOnlyHint: true }
|
|
730
|
+
}, async () => {
|
|
731
|
+
return { content: [{
|
|
732
|
+
type: "text",
|
|
733
|
+
text: `The following components are available in the @primer/react in TypeScript projects:
|
|
734
|
+
|
|
735
|
+
${listComponents().map((component) => {
|
|
736
|
+
return `- ${component.name}`;
|
|
737
|
+
}).join("\n")}
|
|
738
|
+
|
|
739
|
+
You can use the \`get_component\` tool to get more information about a specific component. You can use these components from the @primer/react package.`
|
|
740
|
+
}] };
|
|
741
|
+
});
|
|
742
|
+
server.registerTool("get_component", {
|
|
743
|
+
description: "Retrieve documentation and usage details for a specific React component from the @primer/react package by its name. This tool provides the official Primer documentation for any listed component, making it easy to inspect, reuse, or integrate components in your project.",
|
|
744
|
+
inputSchema: { name: z.string().describe("The name of the component to retrieve") },
|
|
745
|
+
annotations: { readOnlyHint: true }
|
|
746
|
+
}, async ({ name }) => {
|
|
747
|
+
const match = listComponents().find((component) => {
|
|
748
|
+
return component.name === name || component.name.toLowerCase() === name.toLowerCase();
|
|
749
|
+
});
|
|
750
|
+
if (!match) return {
|
|
751
|
+
isError: true,
|
|
752
|
+
errorMessage: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`,
|
|
753
|
+
content: []
|
|
754
|
+
};
|
|
755
|
+
try {
|
|
756
|
+
const llmsUrl = new URL(`/product/components/${match.slug}/llms.txt`, "https://primer.style");
|
|
757
|
+
const llmsResponse = await fetch(llmsUrl);
|
|
758
|
+
if (llmsResponse.ok) return { content: [{
|
|
759
|
+
type: "text",
|
|
760
|
+
text: await llmsResponse.text()
|
|
761
|
+
}] };
|
|
762
|
+
} catch (_) {}
|
|
763
|
+
return {
|
|
764
|
+
isError: true,
|
|
765
|
+
errorMessage: `There was an error fetching documentation for ${name}. Ensure the component exists.`,
|
|
766
|
+
content: []
|
|
767
|
+
};
|
|
768
|
+
});
|
|
769
|
+
server.registerTool("get_component_examples", {
|
|
770
|
+
description: "Get examples for how to use a component from Primer React",
|
|
771
|
+
inputSchema: { name: z.string().describe("The name of the component to retrieve") },
|
|
772
|
+
annotations: { readOnlyHint: true }
|
|
773
|
+
}, async ({ name }) => {
|
|
774
|
+
const match = listComponents().find((component) => {
|
|
775
|
+
return component.name === name;
|
|
776
|
+
});
|
|
777
|
+
if (!match) return { content: [{
|
|
778
|
+
type: "text",
|
|
779
|
+
text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`get_components\` tool.`
|
|
780
|
+
}] };
|
|
781
|
+
const url = new URL(`/product/components/${match.id}`, "https://primer.style");
|
|
782
|
+
const response = await fetch(url);
|
|
783
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
784
|
+
const html = await response.text();
|
|
785
|
+
if (!html) return { content: [] };
|
|
786
|
+
const source = cheerio.load(html)("main").html();
|
|
787
|
+
if (!source) return { content: [] };
|
|
788
|
+
return { content: [{
|
|
789
|
+
type: "text",
|
|
790
|
+
text: `Here are some examples of how to use the \`${name}\` component from the @primer/react package:
|
|
791
|
+
|
|
792
|
+
${turndownService.turndown(source)}`
|
|
793
|
+
}] };
|
|
794
|
+
});
|
|
795
|
+
server.registerTool("get_component_usage_guidelines", {
|
|
796
|
+
description: "Get usage information for how to use a component from Primer",
|
|
797
|
+
inputSchema: { name: z.string().describe("The name of the component to retrieve") },
|
|
798
|
+
annotations: { readOnlyHint: true }
|
|
799
|
+
}, async ({ name }) => {
|
|
800
|
+
const match = listComponents().find((component) => {
|
|
801
|
+
return component.name === name;
|
|
802
|
+
});
|
|
803
|
+
if (!match) return { content: [{
|
|
804
|
+
type: "text",
|
|
805
|
+
text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`get_components\` tool.`
|
|
806
|
+
}] };
|
|
807
|
+
const url = new URL(`/product/components/${match.id}/guidelines`, "https://primer.style");
|
|
808
|
+
const response = await fetch(url);
|
|
809
|
+
if (!response.ok) {
|
|
810
|
+
if (response.status >= 400 && response.status < 500 || response.status >= 300 && response.status < 400) return { content: [{
|
|
811
|
+
type: "text",
|
|
812
|
+
text: `There are no accessibility guidelines for the \`${name}\` component in the @primer/react package.`
|
|
813
|
+
}] };
|
|
814
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
815
|
+
}
|
|
816
|
+
const html = await response.text();
|
|
817
|
+
if (!html) return { content: [] };
|
|
818
|
+
const source = cheerio.load(html)("main").html();
|
|
819
|
+
if (!source) return { content: [] };
|
|
820
|
+
return { content: [{
|
|
821
|
+
type: "text",
|
|
822
|
+
text: `Here are the usage guidelines for the \`${name}\` component from the @primer/react package:
|
|
823
|
+
|
|
824
|
+
${turndownService.turndown(source)}`
|
|
825
|
+
}] };
|
|
826
|
+
});
|
|
827
|
+
server.registerTool("get_component_accessibility_guidelines", {
|
|
828
|
+
description: "Retrieve accessibility guidelines and best practices for a specific component from the @primer/react package by its name. Use this tool to get official accessibility recommendations, usage tips, and requirements to ensure your UI components are inclusive and meet accessibility standards.",
|
|
829
|
+
inputSchema: { name: z.string().describe("The name of the component to retrieve") },
|
|
830
|
+
annotations: { readOnlyHint: true }
|
|
831
|
+
}, async ({ name }) => {
|
|
832
|
+
const match = listComponents().find((component) => {
|
|
833
|
+
return component.name === name;
|
|
834
|
+
});
|
|
835
|
+
if (!match) return { content: [{
|
|
836
|
+
type: "text",
|
|
837
|
+
text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`
|
|
838
|
+
}] };
|
|
839
|
+
const url = new URL(`/product/components/${match.id}/accessibility`, "https://primer.style");
|
|
840
|
+
const response = await fetch(url);
|
|
841
|
+
if (!response.ok) {
|
|
842
|
+
if (response.status >= 400 && response.status < 500 || response.status >= 300 && response.status < 400) return { content: [{
|
|
843
|
+
type: "text",
|
|
844
|
+
text: `There are no accessibility guidelines for the \`${name}\` component in the @primer/react package.`
|
|
845
|
+
}] };
|
|
846
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
847
|
+
}
|
|
848
|
+
const html = await response.text();
|
|
849
|
+
if (!html) return { content: [] };
|
|
850
|
+
const source = cheerio.load(html)("main").html();
|
|
851
|
+
if (!source) return { content: [] };
|
|
852
|
+
return { content: [{
|
|
853
|
+
type: "text",
|
|
854
|
+
text: `Here are the accessibility guidelines for the \`${name}\` component from the @primer/react package:
|
|
855
|
+
|
|
856
|
+
${turndownService.turndown(source)}`
|
|
857
|
+
}] };
|
|
858
|
+
});
|
|
859
|
+
server.registerTool("list_patterns", {
|
|
860
|
+
description: "List all of the patterns available from Primer React. Scenario patterns describe specific user tasks (copy, delete, filter, search). Prefer a scenario pattern when one fits the task, and fall back to the more generic UI patterns otherwise.",
|
|
861
|
+
annotations: { readOnlyHint: true }
|
|
862
|
+
}, async () => {
|
|
863
|
+
const all = listPatterns();
|
|
864
|
+
const scenario = all.filter((pattern) => pattern.category === "scenario").map((pattern) => `- ${pattern.name}`);
|
|
865
|
+
const ui = all.filter((pattern) => pattern.category === "ui").map((pattern) => `- ${pattern.name}`);
|
|
866
|
+
return { content: [{
|
|
867
|
+
type: "text",
|
|
868
|
+
text: `The following patterns are available from \`@primer/react\` for use in TypeScript projects. Scenario patterns describe specific user tasks. Prefer a scenario pattern when one fits the task, and fall back to the UI patterns otherwise.
|
|
869
|
+
|
|
870
|
+
## Scenario patterns
|
|
871
|
+
|
|
872
|
+
${scenario.join("\n")}
|
|
873
|
+
|
|
874
|
+
## UI patterns
|
|
875
|
+
|
|
876
|
+
${ui.join("\n")}`
|
|
877
|
+
}] };
|
|
878
|
+
});
|
|
879
|
+
server.registerTool("get_pattern", {
|
|
880
|
+
description: "Get a specific pattern by name. Scenario patterns describe specific user tasks (copy, delete, filter, search). Prefer a scenario pattern when one fits the task, and fall back to the more generic UI patterns otherwise.",
|
|
881
|
+
inputSchema: { name: z.string().describe("The name of the pattern to retrieve") },
|
|
882
|
+
annotations: { readOnlyHint: true }
|
|
883
|
+
}, async ({ name }) => {
|
|
884
|
+
const patterns = listPatterns();
|
|
885
|
+
const match = patterns.find((pattern) => pattern.category === "scenario" && pattern.name === name) ?? patterns.find((pattern) => pattern.name === name);
|
|
886
|
+
if (!match) return { content: [{
|
|
887
|
+
type: "text",
|
|
888
|
+
text: `There is no pattern named \`${name}\` in the @primer/react package. For a full list of patterns, use the \`list_patterns\` tool.`
|
|
889
|
+
}] };
|
|
890
|
+
const basePath = match.category === "scenario" ? "scenario-patterns" : "ui-patterns";
|
|
891
|
+
const url = new URL(`/product/${basePath}/${match.id}`, "https://primer.style");
|
|
892
|
+
const response = await fetch(url);
|
|
893
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url} - ${response.statusText}`);
|
|
894
|
+
const html = await response.text();
|
|
895
|
+
if (!html) return { content: [] };
|
|
896
|
+
const source = cheerio.load(html)("main").html();
|
|
897
|
+
if (!source) return { content: [] };
|
|
898
|
+
return { content: [{
|
|
899
|
+
type: "text",
|
|
900
|
+
text: `Here are the guidelines for the \`${name}\` pattern for Primer:
|
|
901
|
+
|
|
902
|
+
${turndownService.turndown(source)}`
|
|
903
|
+
}] };
|
|
904
|
+
});
|
|
905
|
+
server.registerTool("find_tokens", {
|
|
906
|
+
description: "Search for specific tokens. Tip: If you only provide a 'group' and leave 'query' empty, it returns all tokens in that category. Avoid property-by-property searching. COLOR RESOLUTION: If a user asks for \"pink\" or \"blue\", do not search for the color name. Use the semantic intent: blue->accent, red->danger, green->success. Always check both \"emphasis\" and \"muted\" variants for background colors. After identifying tokens and writing CSS, you MUST validate the result using lint_css.",
|
|
907
|
+
inputSchema: {
|
|
908
|
+
query: z.string().optional().default("").describe("Search keywords (e.g., \"danger border\", \"success background\")"),
|
|
909
|
+
group: z.string().optional().describe("Filter by group (e.g., \"fgColor\", \"border\")"),
|
|
910
|
+
limit: z.number().int().min(1).max(100).optional().default(15).describe("Maximum results to return to stay within context limits")
|
|
911
|
+
},
|
|
912
|
+
annotations: { readOnlyHint: true }
|
|
913
|
+
}, async ({ query, group, limit }) => {
|
|
914
|
+
const resolvedGroup = group ? GROUP_ALIASES[group.toLowerCase().replace(/\s+/g, "")] || group : void 0;
|
|
915
|
+
const rawKeywords = query.toLowerCase().split(/\s+/).filter((k) => k.length > 0);
|
|
916
|
+
let effectiveGroup = resolvedGroup;
|
|
917
|
+
const filteredKeywords = [];
|
|
918
|
+
for (const kw of rawKeywords) {
|
|
919
|
+
const aliasMatch = GROUP_ALIASES[kw.replace(/\s+/g, "")];
|
|
920
|
+
if (aliasMatch && !effectiveGroup) effectiveGroup = aliasMatch;
|
|
921
|
+
else filteredKeywords.push(kw);
|
|
922
|
+
}
|
|
923
|
+
if (filteredKeywords.length === 0 && !effectiveGroup) return { content: [{
|
|
924
|
+
type: "text",
|
|
925
|
+
text: "Please provide a query, a group, or both. Call `get_design_token_specs` to see available token groups."
|
|
926
|
+
}] };
|
|
927
|
+
const isGroupOnly = filteredKeywords.length === 0 && effectiveGroup;
|
|
928
|
+
let results;
|
|
929
|
+
if (isGroupOnly) results = allTokensWithGuidelines.filter((token) => tokenMatchesGroup(token, effectiveGroup));
|
|
930
|
+
else results = searchTokens(allTokensWithGuidelines, filteredKeywords.join(" "), effectiveGroup);
|
|
931
|
+
if (results.length === 0) {
|
|
932
|
+
const validGroups = getValidGroupsList(allTokensWithGuidelines);
|
|
933
|
+
return { content: [{
|
|
934
|
+
type: "text",
|
|
935
|
+
text: `No tokens found matching "${query}"${effectiveGroup ? ` in group "${effectiveGroup}"` : ""}.
|
|
936
|
+
|
|
937
|
+
### 💡 Available Groups:
|
|
938
|
+
${validGroups}
|
|
939
|
+
|
|
940
|
+
### Troubleshooting for AI:
|
|
941
|
+
1. **Multi-word Queries**: Search keywords use 'AND' logic. If searching "text shorthand typography" fails, try a single keyword like "shorthand" within the "text" group.
|
|
942
|
+
2. **Property Mismatch**: Do not search for CSS properties like "offset", "padding", or "font-size". Use semantic intent keywords: "danger", "muted", "emphasis".
|
|
943
|
+
3. **Typography**: Remember that \`caption\`, \`display\`, and \`code\` groups do NOT support size suffixes. Use the base shorthand only.
|
|
944
|
+
4. **Group Intent**: Use the \`group\` parameter instead of putting group names in the \`query\` string (e.g., use group: "stack" instead of query: "stack padding").`
|
|
945
|
+
}] };
|
|
946
|
+
}
|
|
947
|
+
const limitedResults = results.slice(0, limit);
|
|
948
|
+
let output;
|
|
949
|
+
if (!query) output = `Found ${results.length} token(s). Showing top ${limitedResults.length}:\n\n`;
|
|
950
|
+
else output = `Found ${results.length} token(s) matching "${query}". Showing top ${limitedResults.length}:\n\n`;
|
|
951
|
+
output += formatBundle(limitedResults);
|
|
952
|
+
if (results.length > limit) output += `\n\n*...and ${results.length - limit} more matches. Use more specific keywords to narrow the search.*`;
|
|
953
|
+
return { content: [{
|
|
954
|
+
type: "text",
|
|
955
|
+
text: output
|
|
956
|
+
}] };
|
|
957
|
+
});
|
|
958
|
+
server.registerTool("get_token_group_bundle", {
|
|
959
|
+
description: "PREFERRED FOR COMPONENTS. Fetch all tokens for complex UI (e.g., Dialogs, Cards) in one call by providing an array of groups like ['overlay', 'shadow']. Use this instead of multiple find_tokens calls to save context.",
|
|
960
|
+
inputSchema: { groups: z.array(z.string()).describe("Array of group names (e.g., [\"overlay\", \"shadow\", \"focus\"])") },
|
|
961
|
+
annotations: { readOnlyHint: true }
|
|
962
|
+
}, async ({ groups }) => {
|
|
963
|
+
const resolvedGroups = groups.map((g) => {
|
|
964
|
+
return GROUP_ALIASES[g.toLowerCase().replace(/\s+/g, "")] || g;
|
|
965
|
+
});
|
|
966
|
+
const matched = allTokensWithGuidelines.filter((token) => resolvedGroups.some((rg) => tokenMatchesGroup(token, rg)));
|
|
967
|
+
if (matched.length === 0) {
|
|
968
|
+
const validGroups = getValidGroupsList(allTokensWithGuidelines);
|
|
969
|
+
return { content: [{
|
|
970
|
+
type: "text",
|
|
971
|
+
text: `No tokens found for groups: ${groups.join(", ")}.\n\n### Valid Groups:\n${validGroups}`
|
|
972
|
+
}] };
|
|
973
|
+
}
|
|
974
|
+
let text = `Found ${matched.length} token(s) across ${resolvedGroups.length} group(s):\n\n${formatBundle(matched)}`;
|
|
975
|
+
const activeHints = resolvedGroups.map((g) => groupHints[g]).filter(Boolean);
|
|
976
|
+
if (activeHints.length > 0) text += `\n\n### ⚠️ Usage Guidance:\n${activeHints.map((h) => `- ${h}`).join("\n")}`;
|
|
977
|
+
return { content: [{
|
|
978
|
+
type: "text",
|
|
979
|
+
text
|
|
980
|
+
}] };
|
|
981
|
+
});
|
|
982
|
+
server.registerTool("get_design_token_specs", {
|
|
983
|
+
description: "CRITICAL: CALL THIS FIRST. Provides the logic matrix and the list of valid group names. You cannot search accurately without this map.",
|
|
984
|
+
annotations: { readOnlyHint: true }
|
|
985
|
+
}, async () => {
|
|
986
|
+
const customRules = getDesignTokenSpecsText(listTokenGroups());
|
|
987
|
+
let text;
|
|
988
|
+
try {
|
|
989
|
+
text = `${customRules}\n\n---\n\n${loadDesignTokensGuide()}`;
|
|
990
|
+
} catch {
|
|
991
|
+
text = customRules;
|
|
992
|
+
}
|
|
993
|
+
return { content: [{
|
|
994
|
+
type: "text",
|
|
995
|
+
text
|
|
996
|
+
}] };
|
|
997
|
+
});
|
|
998
|
+
server.registerTool("get_token_usage_patterns", {
|
|
999
|
+
description: "Provides \"Golden Example\" CSS for core patterns: Button (Interactions) and Stack (Layout). Use this to understand how to apply the Logic Matrix, Motion, and Spacing scales.",
|
|
1000
|
+
annotations: { readOnlyHint: true }
|
|
1001
|
+
}, async () => {
|
|
1002
|
+
const customPatterns = getTokenUsagePatternsText();
|
|
1003
|
+
let text;
|
|
1004
|
+
try {
|
|
1005
|
+
const goldenExampleMatch = loadDesignTokensGuide().match(/## Golden Example[\s\S]*?(?=\n## |$)/);
|
|
1006
|
+
if (goldenExampleMatch) text = `${customPatterns}\n\n---\n\n${goldenExampleMatch[0].trim()}`;
|
|
1007
|
+
else text = customPatterns;
|
|
1008
|
+
} catch {
|
|
1009
|
+
text = customPatterns;
|
|
1010
|
+
}
|
|
1011
|
+
return { content: [{
|
|
1012
|
+
type: "text",
|
|
1013
|
+
text
|
|
1014
|
+
}] };
|
|
1015
|
+
});
|
|
1016
|
+
server.registerTool("lint_css", {
|
|
1017
|
+
description: "REQUIRED FINAL STEP. Use this to validate your CSS. You cannot complete a task involving CSS without a successful run of this tool.",
|
|
1018
|
+
inputSchema: { css: z.string() },
|
|
1019
|
+
annotations: { readOnlyHint: true }
|
|
1020
|
+
}, async ({ css }) => {
|
|
1021
|
+
try {
|
|
1022
|
+
const { stdout } = await runStylelint(css);
|
|
1023
|
+
return { content: [{
|
|
1024
|
+
type: "text",
|
|
1025
|
+
text: stdout || "✅ Stylelint passed (or was successfully autofixed)."
|
|
1026
|
+
}] };
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
return { content: [{
|
|
1029
|
+
type: "text",
|
|
1030
|
+
text: `❌ Errors without autofix remaining:\n${error instanceof Error && "stdout" in error ? error.stdout : String(error)}`
|
|
1031
|
+
}] };
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
server.registerTool("get_color_usage", {
|
|
1035
|
+
description: "Get the guidelines for how to apply color to a user interface",
|
|
1036
|
+
annotations: { readOnlyHint: true }
|
|
1037
|
+
}, async () => {
|
|
1038
|
+
const url = new URL(`/product/getting-started/foundations/color-usage`, "https://primer.style");
|
|
1039
|
+
const response = await fetch(url);
|
|
1040
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url} - ${response.statusText}`);
|
|
1041
|
+
const html = await response.text();
|
|
1042
|
+
if (!html) return { content: [] };
|
|
1043
|
+
const source = cheerio.load(html)("main").html();
|
|
1044
|
+
if (!source) return { content: [] };
|
|
1045
|
+
return { content: [{
|
|
1046
|
+
type: "text",
|
|
1047
|
+
text: `Here is the documentation for color usage in Primer:\n\n${turndownService.turndown(source)}`
|
|
1048
|
+
}] };
|
|
1049
|
+
});
|
|
1050
|
+
server.registerTool("get_typography_usage", {
|
|
1051
|
+
description: "Get the guidelines for how to apply typography to a user interface",
|
|
1052
|
+
annotations: { readOnlyHint: true }
|
|
1053
|
+
}, async () => {
|
|
1054
|
+
const url = new URL(`/product/getting-started/foundations/typography`, "https://primer.style");
|
|
1055
|
+
const response = await fetch(url);
|
|
1056
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url} - ${response.statusText}`);
|
|
1057
|
+
const html = await response.text();
|
|
1058
|
+
if (!html) return { content: [] };
|
|
1059
|
+
const source = cheerio.load(html)("main").html();
|
|
1060
|
+
if (!source) return { content: [] };
|
|
1061
|
+
return { content: [{
|
|
1062
|
+
type: "text",
|
|
1063
|
+
text: `Here is the documentation for typography usage in Primer:\n\n${turndownService.turndown(source)}`
|
|
1064
|
+
}] };
|
|
1065
|
+
});
|
|
1066
|
+
server.registerTool("list_icons", {
|
|
1067
|
+
description: "List all of the icons (octicons) available from Primer Octicons React",
|
|
1068
|
+
annotations: { readOnlyHint: true }
|
|
1069
|
+
}, async () => {
|
|
1070
|
+
return { content: [{
|
|
1071
|
+
type: "text",
|
|
1072
|
+
text: `The following icons are available in the @primer/octicons-react package in TypeScript projects:
|
|
1073
|
+
|
|
1074
|
+
${listIcons().map((icon) => {
|
|
1075
|
+
const keywords = icon.keywords.map((keyword) => {
|
|
1076
|
+
return `<keyword>${keyword}</keyword>`;
|
|
1077
|
+
});
|
|
1078
|
+
const sizes = icon.heights.map((height) => {
|
|
1079
|
+
return `<size value="${height}"></size>`;
|
|
1080
|
+
});
|
|
1081
|
+
return [
|
|
1082
|
+
`<icon name="${icon.name}">`,
|
|
1083
|
+
...keywords,
|
|
1084
|
+
...sizes,
|
|
1085
|
+
`</icon>`
|
|
1086
|
+
].join("\n");
|
|
1087
|
+
}).join("\n")}
|
|
1088
|
+
|
|
1089
|
+
You can use the \`get_icon\` tool to get more information about a specific icon. You can use these components from the @primer/octicons-react package.`
|
|
1090
|
+
}] };
|
|
1091
|
+
});
|
|
1092
|
+
server.registerTool("get_icon", {
|
|
1093
|
+
description: "Get a specific icon (octicon) by name from Primer",
|
|
1094
|
+
inputSchema: {
|
|
1095
|
+
name: z.string().describe("The name of the icon to retrieve"),
|
|
1096
|
+
size: z.string().optional().describe("The size of the icon to retrieve, e.g. \"16\"").default("16")
|
|
1097
|
+
},
|
|
1098
|
+
annotations: { readOnlyHint: true }
|
|
1099
|
+
}, async ({ name, size }) => {
|
|
1100
|
+
const match = listIcons().find((icon) => {
|
|
1101
|
+
return icon.name === name || icon.name.toLowerCase() === name.toLowerCase();
|
|
1102
|
+
});
|
|
1103
|
+
if (!match) return { content: [{
|
|
1104
|
+
type: "text",
|
|
1105
|
+
text: `There is no icon named \`${name}\` in the @primer/octicons-react package. For a full list of icons, use the \`get_icon\` tool.`
|
|
1106
|
+
}] };
|
|
1107
|
+
const url = new URL(`/octicons/icon/${match.name}-${size}`, "https://primer.style");
|
|
1108
|
+
const response = await fetch(url);
|
|
1109
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
1110
|
+
const html = await response.text();
|
|
1111
|
+
if (!html) return { content: [] };
|
|
1112
|
+
const source = cheerio.load(html)("main").html();
|
|
1113
|
+
if (!source) return { content: [] };
|
|
1114
|
+
return { content: [{
|
|
1115
|
+
type: "text",
|
|
1116
|
+
text: `Here is the documentation for the \`${name}\` icon at size: \`${size}\`:
|
|
1117
|
+
${turndownService.turndown(source)}`
|
|
1118
|
+
}] };
|
|
1119
|
+
});
|
|
1120
|
+
server.registerTool("primer_coding_guidelines", {
|
|
1121
|
+
description: "Get the guidelines when writing code that uses Primer or for UI code that you are creating",
|
|
1122
|
+
annotations: { readOnlyHint: true }
|
|
1123
|
+
}, async () => {
|
|
1124
|
+
return { content: [{
|
|
1125
|
+
type: "text",
|
|
1126
|
+
text: `When writing code that uses Primer, follow these guidelines:
|
|
1127
|
+
|
|
1128
|
+
## Design Tokens
|
|
1129
|
+
|
|
1130
|
+
- Prefer design tokens over hard-coded values. For example, use \`var(--fgColor-default)\` instead of \`#24292f\`. Use the \`find_tokens\` tool to search for a design token by keyword or group. Use \`get_design_token_specs\` to browse available token groups, and \`get_token_group_bundle\` to retrieve all tokens within a specific group.
|
|
1131
|
+
- Prefer recommending design tokens in the same group for related CSS properties. For example, when styling background and border color, use tokens from the same group/category
|
|
1132
|
+
|
|
1133
|
+
## Authoring & Using Components
|
|
1134
|
+
|
|
1135
|
+
- Prefer re-using a component from Primer when possible over writing a new component.
|
|
1136
|
+
- Prefer using existing props for a component for styling instead of adding styling to a component
|
|
1137
|
+
- Prefer using icons from Primer instead of creating new icons. Use the \`list_icons\` tool to find the icon you need.
|
|
1138
|
+
- Follow patterns from Primer when creating new components. Use the \`list_patterns\` tool to find the pattern you need, if one exists
|
|
1139
|
+
- When using a component from Primer, make sure to follow the component's usage and accessibility guidelines
|
|
1140
|
+
|
|
1141
|
+
## Coding guidelines
|
|
1142
|
+
|
|
1143
|
+
The following list of coding guidelines must be followed:
|
|
1144
|
+
|
|
1145
|
+
- Do not use the sx prop for styling components. Instead, use CSS Modules.
|
|
1146
|
+
- Do not use the Box component for styling components. Instead, use CSS Modules.
|
|
1147
|
+
`
|
|
1148
|
+
}] };
|
|
1149
|
+
});
|
|
1150
|
+
/**
|
|
1151
|
+
* The `review_alt_text` tool is experimental and may be removed in future versions.
|
|
1152
|
+
*
|
|
1153
|
+
* The intent of this tool is to assist products like Copilot Code Review and Copilot Coding Agent
|
|
1154
|
+
* in reviewing both user- and AI-generated alt text for images, ensuring compliance with accessibility guidelines.
|
|
1155
|
+
* This tool is not intended to replace human-generated alt text; rather, it supports the review process
|
|
1156
|
+
* by providing suggestions for improvement. It should be used alongside human review, not as a substitute.
|
|
1157
|
+
*
|
|
1158
|
+
*
|
|
1159
|
+
**/
|
|
1160
|
+
server.registerTool("review_alt_text", {
|
|
1161
|
+
description: "Evaluates image alt text against accessibility best practices and context relevance.",
|
|
1162
|
+
inputSchema: {
|
|
1163
|
+
surroundingText: z.string().describe("Text surrounding the image, relevant to the image."),
|
|
1164
|
+
alt: z.string().describe("The alt text of the image being evaluated"),
|
|
1165
|
+
image: z.string().describe("The image URL or file path being evaluated")
|
|
1166
|
+
},
|
|
1167
|
+
annotations: { readOnlyHint: true }
|
|
1168
|
+
}, async ({ surroundingText, alt, image }) => {
|
|
1169
|
+
const response = await server.server.createMessage({
|
|
1170
|
+
messages: [{
|
|
1171
|
+
role: "user",
|
|
1172
|
+
content: {
|
|
1173
|
+
type: "text",
|
|
1174
|
+
text: `Does this alt text: '${alt}' meet accessibility guidelines and describe the image: ${image} accurately in context of this surrounding text: '${surroundingText}'?\n\n`
|
|
1175
|
+
}
|
|
1176
|
+
}],
|
|
1177
|
+
sampling: { temperature: .4 },
|
|
1178
|
+
maxTokens: 500
|
|
1179
|
+
});
|
|
1180
|
+
return {
|
|
1181
|
+
content: [{
|
|
1182
|
+
type: "text",
|
|
1183
|
+
text: response.content.type === "text" ? response.content.text : "Unable to generate summary"
|
|
1184
|
+
}],
|
|
1185
|
+
altTextEvaluation: response.content.type === "text" ? response.content.text : "Unable to generate summary",
|
|
1186
|
+
nextSteps: `If the evaluation indicates issues with the alt text, provide more meaningful alt text based on the feedback. DO NOT run this tool repeatedly on the same image - evaluations may vary slightly with each run.`
|
|
1187
|
+
};
|
|
1188
|
+
});
|
|
1189
|
+
//#endregion
|
|
1190
|
+
export { server as t };
|