@nysds/mcp-server 1.0.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/index.d.ts +9 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +16 -0
  4. package/dist/lib/cem-parser.d.ts +96 -0
  5. package/dist/lib/cem-parser.d.ts.map +1 -0
  6. package/dist/lib/cem-parser.js +82 -0
  7. package/dist/lib/search.d.ts +20 -0
  8. package/dist/lib/search.d.ts.map +1 -0
  9. package/dist/lib/search.js +87 -0
  10. package/dist/lib/token-parser.d.ts +140 -0
  11. package/dist/lib/token-parser.d.ts.map +1 -0
  12. package/dist/lib/token-parser.js +458 -0
  13. package/dist/prompts/nysds-mode.d.ts +8 -0
  14. package/dist/prompts/nysds-mode.d.ts.map +1 -0
  15. package/dist/prompts/nysds-mode.js +51 -0
  16. package/dist/resources/components.d.ts +12 -0
  17. package/dist/resources/components.d.ts.map +1 -0
  18. package/dist/resources/components.js +62 -0
  19. package/dist/resources/guides.d.ts +11 -0
  20. package/dist/resources/guides.d.ts.map +1 -0
  21. package/dist/resources/guides.js +80 -0
  22. package/dist/resources/index.d.ts +8 -0
  23. package/dist/resources/index.d.ts.map +1 -0
  24. package/dist/resources/index.js +13 -0
  25. package/dist/resources/tokens.d.ts +17 -0
  26. package/dist/resources/tokens.d.ts.map +1 -0
  27. package/dist/resources/tokens.js +312 -0
  28. package/dist/server.d.ts +11 -0
  29. package/dist/server.d.ts.map +1 -0
  30. package/dist/server.js +30 -0
  31. package/dist/tools/component-tools.d.ts +14 -0
  32. package/dist/tools/component-tools.d.ts.map +1 -0
  33. package/dist/tools/component-tools.js +127 -0
  34. package/dist/tools/index.d.ts +8 -0
  35. package/dist/tools/index.d.ts.map +1 -0
  36. package/dist/tools/index.js +13 -0
  37. package/dist/tools/token-tools.d.ts +17 -0
  38. package/dist/tools/token-tools.d.ts.map +1 -0
  39. package/dist/tools/token-tools.js +504 -0
  40. package/dist/tools/validation-tools.d.ts +12 -0
  41. package/dist/tools/validation-tools.d.ts.map +1 -0
  42. package/dist/tools/validation-tools.js +222 -0
  43. package/package.json +1 -1
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Token Tools
3
+ *
4
+ * MCP tools for working with NYSDS design tokens.
5
+ *
6
+ * Tools:
7
+ * - get_design_tokens: Get tokens with filtering and options
8
+ * - find_tokens: Search tokens by CSS variable, value, or description
9
+ * - list_themes: List all 8 agency themes
10
+ * - get_token_value: Get detailed info for a specific token
11
+ * - validate_token_usage: Validate token usage in context
12
+ * - list_token_categories: List categories with token counts
13
+ * - get_token_graph: Get token dependency graph
14
+ */
15
+ import { z } from "zod";
16
+ import { getCSSTokens, getCSSTokensByCategory, getCSSTokensByLayer, searchCSSTokens, getCSSTokenByVariable, getCSSTokenCategories, buildTokenGraph, } from "../lib/token-parser.js";
17
+ const AGENCY_THEMES = [
18
+ {
19
+ id: "default",
20
+ name: "Default (NY.gov)",
21
+ description: "Default NY.gov theme for general state sites",
22
+ primaryColor: "#154973",
23
+ selectors: [":root", "[data-theme='default']", ".nys-theme-default"],
24
+ },
25
+ {
26
+ id: "admin",
27
+ name: "Administration",
28
+ description: "For administrative and regulatory agencies",
29
+ primaryColor: "#a2350a",
30
+ selectors: ["[data-theme='admin']", ".nys-theme-admin"],
31
+ },
32
+ {
33
+ id: "business",
34
+ name: "Business",
35
+ description: "For commerce and economic development agencies",
36
+ primaryColor: "#084b52",
37
+ selectors: ["[data-theme='business']", ".nys-theme-business"],
38
+ },
39
+ {
40
+ id: "environment",
41
+ name: "Environment",
42
+ description: "For environmental and conservation agencies",
43
+ primaryColor: "#233f2b",
44
+ selectors: ["[data-theme='environment']", ".nys-theme-environment"],
45
+ },
46
+ {
47
+ id: "health",
48
+ name: "Health",
49
+ description: "For health and human services agencies",
50
+ primaryColor: "#43285d",
51
+ selectors: ["[data-theme='health']", ".nys-theme-health"],
52
+ },
53
+ {
54
+ id: "local",
55
+ name: "Local Government",
56
+ description: "For local government entities",
57
+ primaryColor: "#402217",
58
+ selectors: ["[data-theme='local']", ".nys-theme-local"],
59
+ },
60
+ {
61
+ id: "safety",
62
+ name: "Public Safety",
63
+ description: "For public safety agencies",
64
+ primaryColor: "#435d6e",
65
+ selectors: ["[data-theme='safety']", ".nys-theme-safety"],
66
+ },
67
+ {
68
+ id: "transportation",
69
+ name: "Transportation",
70
+ description: "For DOT and transit authorities",
71
+ primaryColor: "#0b5583",
72
+ selectors: ["[data-theme='transportation']", ".nys-theme-transportation"],
73
+ },
74
+ ];
75
+ /**
76
+ * Get recommended pairings and validation for a token in a specific context
77
+ */
78
+ function validateTokenInContext(token, context) {
79
+ const recommendations = [];
80
+ const alternatives = [];
81
+ const warnings = [];
82
+ let valid = true;
83
+ const varName = token.cssVariable;
84
+ // Context-specific validation rules
85
+ if (context === "text") {
86
+ if (varName.includes("-surface-")) {
87
+ valid = false;
88
+ warnings.push("Surface colors should not be used for text. Use --nys-color-text-* tokens.");
89
+ alternatives.push("--nys-color-text", "--nys-color-text-weak", "--nys-color-text-weaker");
90
+ }
91
+ else if (varName.includes("-text-")) {
92
+ recommendations.push("Pair with --nys-color-surface for optimal contrast on light backgrounds");
93
+ recommendations.push("Pair with --nys-color-surface-reverse for dark backgrounds");
94
+ }
95
+ else if (varName.includes("-link-")) {
96
+ recommendations.push("Ensure links are distinguishable from body text");
97
+ recommendations.push("Include visible focus and hover states");
98
+ }
99
+ else if (varName.includes("-danger-") || varName.includes("-warning-")) {
100
+ recommendations.push("Pair with corresponding icon for accessibility");
101
+ }
102
+ }
103
+ else if (context === "background") {
104
+ if (varName.includes("-text-")) {
105
+ valid = false;
106
+ warnings.push("Text colors should not be used for backgrounds. Use --nys-color-surface-* tokens.");
107
+ alternatives.push("--nys-color-surface", "--nys-color-surface-raised", "--nys-color-surface-reverse");
108
+ }
109
+ else if (varName.includes("-surface-")) {
110
+ if (varName.includes("-reverse")) {
111
+ recommendations.push("Use --nys-color-text-reverse for text on dark surfaces");
112
+ }
113
+ else {
114
+ recommendations.push("Use --nys-color-text for text on light surfaces");
115
+ }
116
+ }
117
+ else if (varName.includes("-weak") &&
118
+ (varName.includes("-success-") ||
119
+ varName.includes("-info-") ||
120
+ varName.includes("-warning-") ||
121
+ varName.includes("-danger-"))) {
122
+ recommendations.push("Use for alert/notification backgrounds");
123
+ recommendations.push("Pair with corresponding -strong variant for text color");
124
+ }
125
+ }
126
+ else if (context === "border") {
127
+ if (varName.includes("-base-")) {
128
+ recommendations.push("Good for borders and dividers");
129
+ }
130
+ else if (varName.includes("-focus")) {
131
+ recommendations.push("Reserved for focus ring outlines");
132
+ recommendations.push("Use 2px solid for visible focus indication");
133
+ }
134
+ else if (varName.includes("-theme-")) {
135
+ recommendations.push("Use for branded accent borders");
136
+ }
137
+ }
138
+ else if (context === "icon") {
139
+ if (varName.includes("-ink-")) {
140
+ recommendations.push("Primary choice for icons");
141
+ if (varName.includes("-reverse")) {
142
+ recommendations.push("Use on dark backgrounds");
143
+ }
144
+ }
145
+ else if (varName.includes("-text-")) {
146
+ recommendations.push("Icons can use text colors for consistency");
147
+ }
148
+ else if (varName.includes("-success-") ||
149
+ varName.includes("-danger-") ||
150
+ varName.includes("-warning-") ||
151
+ varName.includes("-info-")) {
152
+ recommendations.push("Use for status icons to reinforce meaning with color");
153
+ }
154
+ }
155
+ // Find similar tokens as alternatives
156
+ if (alternatives.length === 0) {
157
+ const tokens = getCSSTokens();
158
+ const category = token.category;
159
+ const similar = tokens
160
+ .filter((t) => t.category === category &&
161
+ t.cssVariable !== varName &&
162
+ ((context === "text" && t.cssVariable.includes("-text-")) ||
163
+ (context === "background" && t.cssVariable.includes("-surface-")) ||
164
+ (context === "border" && t.cssVariable.includes("-base-")) ||
165
+ (context === "icon" && t.cssVariable.includes("-ink-"))))
166
+ .slice(0, 3)
167
+ .map((t) => t.cssVariable);
168
+ alternatives.push(...similar);
169
+ }
170
+ return {
171
+ valid,
172
+ cssVariable: token.cssVariable,
173
+ context,
174
+ recommendations,
175
+ alternatives,
176
+ warnings: warnings.length > 0 ? warnings : undefined,
177
+ };
178
+ }
179
+ // ============================================================================
180
+ // Tool Registration
181
+ // ============================================================================
182
+ export function registerTokenTools(server) {
183
+ // -------------------------------------------------------------------------
184
+ // get_design_tokens - Enhanced token retrieval
185
+ // -------------------------------------------------------------------------
186
+ server.tool("get_design_tokens", "Get NYSDS design tokens with optional filtering by category or layer. Returns CSS variable names and values.", {
187
+ category: z
188
+ .enum([
189
+ "color",
190
+ "font",
191
+ "space",
192
+ "size",
193
+ "radius",
194
+ "shadow",
195
+ "border",
196
+ "form",
197
+ "gutter",
198
+ "icon",
199
+ ])
200
+ .optional()
201
+ .describe("Filter by token category"),
202
+ layer: z
203
+ .enum(["primitive", "applied", "theme", "appearance"])
204
+ .optional()
205
+ .describe("Filter by token layer (primitive = raw values, applied = semantic tokens, theme = agency theme tokens, appearance = light/dark mode)"),
206
+ includeResolvedValues: z
207
+ .boolean()
208
+ .default(false)
209
+ .describe("Include resolved hex/rem values for previews"),
210
+ includeDescriptions: z
211
+ .boolean()
212
+ .default(true)
213
+ .describe("Include token descriptions"),
214
+ }, async ({ category, layer, includeResolvedValues, includeDescriptions }) => {
215
+ let tokens;
216
+ if (category) {
217
+ tokens = getCSSTokensByCategory(category);
218
+ }
219
+ else if (layer) {
220
+ tokens = getCSSTokensByLayer(layer);
221
+ }
222
+ else {
223
+ tokens = getCSSTokens();
224
+ }
225
+ // Format output based on options
226
+ const formatted = tokens.map((t) => {
227
+ const result = {
228
+ cssVariable: t.cssVariable,
229
+ cssValue: t.cssValue,
230
+ };
231
+ if (includeResolvedValues && t.resolvedValue) {
232
+ result.resolvedValue = t.resolvedValue;
233
+ }
234
+ if (includeDescriptions && t.description) {
235
+ result.description = t.description;
236
+ }
237
+ return result;
238
+ });
239
+ return {
240
+ content: [
241
+ {
242
+ type: "text",
243
+ text: JSON.stringify({
244
+ count: tokens.length,
245
+ tokens: formatted,
246
+ }, null, 2),
247
+ },
248
+ ],
249
+ };
250
+ });
251
+ // -------------------------------------------------------------------------
252
+ // find_tokens - Search tokens
253
+ // -------------------------------------------------------------------------
254
+ server.tool("find_tokens", "Search for design tokens by CSS variable name, value, or description", {
255
+ query: z
256
+ .string()
257
+ .describe("Search query to match against CSS variable names, values, and descriptions"),
258
+ category: z
259
+ .enum([
260
+ "color",
261
+ "font",
262
+ "space",
263
+ "size",
264
+ "radius",
265
+ "shadow",
266
+ "border",
267
+ "form",
268
+ "gutter",
269
+ "icon",
270
+ ])
271
+ .optional()
272
+ .describe("Optionally filter results to a specific category"),
273
+ }, async ({ query, category }) => {
274
+ let results = searchCSSTokens(query);
275
+ if (category) {
276
+ results = results.filter((t) => t.category.toLowerCase() === category.toLowerCase());
277
+ }
278
+ const formatted = results.map((t) => ({
279
+ cssVariable: t.cssVariable,
280
+ cssValue: t.cssValue,
281
+ description: t.description,
282
+ category: t.category,
283
+ }));
284
+ return {
285
+ content: [
286
+ {
287
+ type: "text",
288
+ text: JSON.stringify({
289
+ query,
290
+ count: results.length,
291
+ results: formatted,
292
+ }, null, 2),
293
+ },
294
+ ],
295
+ };
296
+ });
297
+ // -------------------------------------------------------------------------
298
+ // list_themes - List agency themes
299
+ // -------------------------------------------------------------------------
300
+ server.tool("list_themes", "List all NYSDS agency themes with their selectors and primary colors", {}, async () => {
301
+ return {
302
+ content: [
303
+ {
304
+ type: "text",
305
+ text: JSON.stringify({
306
+ themes: AGENCY_THEMES,
307
+ usage: {
308
+ htmlAttribute: '<html data-theme="admin">',
309
+ cssClass: '<body class="nys-theme-admin">',
310
+ cssCustomProperty: "Themes modify --nys-color-theme-* tokens",
311
+ },
312
+ totalThemes: AGENCY_THEMES.length,
313
+ }, null, 2),
314
+ },
315
+ ],
316
+ };
317
+ });
318
+ // -------------------------------------------------------------------------
319
+ // get_token_value - Get detailed token info
320
+ // -------------------------------------------------------------------------
321
+ server.tool("get_token_value", "Get detailed information about a specific design token including its value, description, and which tokens use it", {
322
+ token: z
323
+ .string()
324
+ .describe('CSS variable name (e.g., "--nys-color-text" or "color-text")'),
325
+ }, async ({ token }) => {
326
+ const tokenInfo = getCSSTokenByVariable(token);
327
+ if (!tokenInfo) {
328
+ return {
329
+ content: [
330
+ {
331
+ type: "text",
332
+ text: JSON.stringify({
333
+ error: `Token not found: ${token}`,
334
+ suggestion: "Try searching with find_tokens to discover available tokens",
335
+ }, null, 2),
336
+ },
337
+ ],
338
+ };
339
+ }
340
+ // Get dependents (tokens that use this one)
341
+ const graph = buildTokenGraph();
342
+ const node = graph.get(tokenInfo.cssVariable);
343
+ const usedBy = node?.usedBy || [];
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: JSON.stringify({
349
+ cssVariable: tokenInfo.cssVariable,
350
+ cssValue: tokenInfo.cssValue,
351
+ resolvedValue: tokenInfo.resolvedValue,
352
+ description: tokenInfo.description,
353
+ category: tokenInfo.category,
354
+ layer: tokenInfo.layer,
355
+ usedBy: usedBy.length > 0 ? usedBy : undefined,
356
+ usage: `var(${tokenInfo.cssVariable})`,
357
+ }, null, 2),
358
+ },
359
+ ],
360
+ };
361
+ });
362
+ // -------------------------------------------------------------------------
363
+ // validate_token_usage - Validate token in context
364
+ // -------------------------------------------------------------------------
365
+ server.tool("validate_token_usage", "Validate whether a token is appropriate for a specific usage context (text, background, border, icon)", {
366
+ token: z
367
+ .string()
368
+ .describe('CSS variable name (e.g., "--nys-color-text" or "color-text")'),
369
+ context: z
370
+ .enum(["text", "background", "border", "icon"])
371
+ .describe("How the token will be used"),
372
+ }, async ({ token, context }) => {
373
+ const tokenInfo = getCSSTokenByVariable(token);
374
+ if (!tokenInfo) {
375
+ return {
376
+ content: [
377
+ {
378
+ type: "text",
379
+ text: JSON.stringify({
380
+ error: `Token not found: ${token}`,
381
+ suggestion: "Try searching with find_tokens to discover available tokens",
382
+ }, null, 2),
383
+ },
384
+ ],
385
+ };
386
+ }
387
+ const validation = validateTokenInContext(tokenInfo, context);
388
+ return {
389
+ content: [
390
+ {
391
+ type: "text",
392
+ text: JSON.stringify(validation, null, 2),
393
+ },
394
+ ],
395
+ };
396
+ });
397
+ // -------------------------------------------------------------------------
398
+ // list_token_categories - List categories with counts
399
+ // -------------------------------------------------------------------------
400
+ server.tool("list_token_categories", "List all token categories with the number of tokens in each", {}, async () => {
401
+ const categories = getCSSTokenCategories();
402
+ const total = categories.reduce((sum, c) => sum + c.count, 0);
403
+ return {
404
+ content: [
405
+ {
406
+ type: "text",
407
+ text: JSON.stringify({
408
+ categories,
409
+ totalTokens: total,
410
+ }, null, 2),
411
+ },
412
+ ],
413
+ };
414
+ });
415
+ // -------------------------------------------------------------------------
416
+ // get_token_graph - Token dependency graph
417
+ // -------------------------------------------------------------------------
418
+ server.tool("get_token_graph", "Get the token dependency graph showing which tokens reference which and which are used by which", {
419
+ filter: z
420
+ .enum(["all", "aliases-only", "primitives-only", "most-used"])
421
+ .default("all")
422
+ .describe("Filter the graph to specific token types"),
423
+ category: z
424
+ .enum([
425
+ "color",
426
+ "font",
427
+ "space",
428
+ "size",
429
+ "radius",
430
+ "shadow",
431
+ "border",
432
+ "form",
433
+ "gutter",
434
+ "icon",
435
+ ])
436
+ .optional()
437
+ .describe("Filter to a specific category"),
438
+ limit: z
439
+ .number()
440
+ .int()
441
+ .min(1)
442
+ .max(100)
443
+ .default(50)
444
+ .describe("Maximum number of tokens to return"),
445
+ }, async ({ filter, category, limit }) => {
446
+ const graph = buildTokenGraph();
447
+ const tokens = getCSSTokens();
448
+ // Build lookup for categories
449
+ const tokenCategories = new Map();
450
+ for (const t of tokens) {
451
+ tokenCategories.set(t.cssVariable, t.category);
452
+ }
453
+ let nodes = Array.from(graph.values());
454
+ // Apply category filter
455
+ if (category) {
456
+ nodes = nodes.filter((n) => tokenCategories.get(n.cssVariable) === category);
457
+ }
458
+ // Apply type filter
459
+ if (filter === "aliases-only") {
460
+ nodes = nodes.filter((n) => n.referencesVariable);
461
+ }
462
+ else if (filter === "primitives-only") {
463
+ nodes = nodes.filter((n) => !n.referencesVariable);
464
+ }
465
+ else if (filter === "most-used") {
466
+ nodes = nodes.filter((n) => n.usedBy.length > 0);
467
+ nodes.sort((a, b) => b.usedBy.length - a.usedBy.length);
468
+ }
469
+ // Apply limit
470
+ nodes = nodes.slice(0, limit);
471
+ // Calculate stats
472
+ const allNodes = Array.from(graph.values());
473
+ const stats = {
474
+ totalTokens: allNodes.length,
475
+ aliasTokens: allNodes.filter((n) => n.referencesVariable).length,
476
+ primitiveTokens: allNodes.filter((n) => !n.referencesVariable).length,
477
+ mostUsed: allNodes
478
+ .filter((n) => n.usedBy.length > 0)
479
+ .sort((a, b) => b.usedBy.length - a.usedBy.length)
480
+ .slice(0, 5)
481
+ .map((n) => ({
482
+ cssVariable: n.cssVariable,
483
+ usedByCount: n.usedBy.length,
484
+ })),
485
+ };
486
+ return {
487
+ content: [
488
+ {
489
+ type: "text",
490
+ text: JSON.stringify({
491
+ filter,
492
+ category: category || "all",
493
+ nodes: nodes.map((n) => ({
494
+ cssVariable: n.cssVariable,
495
+ referencesVariable: n.referencesVariable,
496
+ usedBy: n.usedBy.length > 0 ? n.usedBy : undefined,
497
+ })),
498
+ stats,
499
+ }, null, 2),
500
+ },
501
+ ],
502
+ };
503
+ });
504
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Validation Tools
3
+ *
4
+ * MCP tools for validating NYSDS component usage.
5
+ *
6
+ * P1 Tools:
7
+ * - validate_component_api: Validate prop/attribute usage
8
+ * - setup_framework: Framework-specific guides
9
+ */
10
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ export declare function registerValidationTools(server: McpServer): void;
12
+ //# sourceMappingURL=validation-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation-tools.d.ts","sourceRoot":"","sources":["../../src/tools/validation-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAyO/D"}