@nysds/mcp-server 1.18.0 → 1.18.3

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.
@@ -4,312 +4,74 @@
4
4
  * MCP tools for working with NYSDS design tokens.
5
5
  *
6
6
  * Tools:
7
- * - get_tokens: Get tokens, categories, or themes
8
- * - find_tokens: Search tokens by CSS variable, value, or description
9
- * - get_token_info: Get detailed token info with optional context validation
7
+ * - find_tokens: Search or browse tokens (discovery)
8
+ * - get_token: Get full details for a specific token (deep dive)
10
9
  * - get_token_graph: Get token dependency graph
11
10
  */
12
11
  import { z } from "zod";
13
- import { getCSSTokens, getCSSTokensByCategory, getCSSTokensByLayer, searchCSSTokens, getCSSTokenByVariable, getCSSTokenCategories, buildTokenGraph, } from "../lib/token-parser.js";
14
- import { formatResponse } from "../lib/format.js";
15
- const AGENCY_THEMES = [
16
- {
17
- id: "default",
18
- name: "Default (NY.gov)",
19
- description: "Default NY.gov theme for general state sites",
20
- primaryColor: "#154973",
21
- selectors: [":root", "[data-theme='default']", ".nys-theme-default"],
22
- },
23
- {
24
- id: "admin",
25
- name: "Administration",
26
- description: "For administrative and regulatory agencies",
27
- primaryColor: "#a2350a",
28
- selectors: ["[data-theme='admin']", ".nys-theme-admin"],
29
- },
30
- {
31
- id: "business",
32
- name: "Business",
33
- description: "For commerce and economic development agencies",
34
- primaryColor: "#084b52",
35
- selectors: ["[data-theme='business']", ".nys-theme-business"],
36
- },
37
- {
38
- id: "environment",
39
- name: "Environment",
40
- description: "For environmental and conservation agencies",
41
- primaryColor: "#233f2b",
42
- selectors: ["[data-theme='environment']", ".nys-theme-environment"],
43
- },
44
- {
45
- id: "health",
46
- name: "Health",
47
- description: "For health and human services agencies",
48
- primaryColor: "#43285d",
49
- selectors: ["[data-theme='health']", ".nys-theme-health"],
50
- },
51
- {
52
- id: "local",
53
- name: "Local Government",
54
- description: "For local government entities",
55
- primaryColor: "#402217",
56
- selectors: ["[data-theme='local']", ".nys-theme-local"],
57
- },
58
- {
59
- id: "safety",
60
- name: "Public Safety",
61
- description: "For public safety agencies",
62
- primaryColor: "#435d6e",
63
- selectors: ["[data-theme='safety']", ".nys-theme-safety"],
64
- },
65
- {
66
- id: "transportation",
67
- name: "Transportation",
68
- description: "For DOT and transit authorities",
69
- primaryColor: "#0b5583",
70
- selectors: ["[data-theme='transportation']", ".nys-theme-transportation"],
71
- },
12
+ import { getCSSTokens, getCSSTokenByVariable, buildTokenGraph, getRecommendedTokens, getPrimitiveTokens, getAlternativeTokens, } from "../lib/token-parser.js";
13
+ // ============================================================================
14
+ // Shared Constants
15
+ // ============================================================================
16
+ const CATEGORY_ENUM = [
17
+ "color",
18
+ "font",
19
+ "space",
20
+ "size",
21
+ "radius",
22
+ "shadow",
23
+ "border",
24
+ "form",
25
+ "gutter",
26
+ "icon",
72
27
  ];
73
- /**
74
- * Get recommended pairings and validation for a token in a specific context
75
- */
76
- function validateTokenInContext(token, context) {
77
- const recommendations = [];
78
- const alternatives = [];
79
- const warnings = [];
80
- let valid = true;
81
- const varName = token.cssVariable;
82
- // Context-specific validation rules
83
- if (context === "text") {
84
- if (varName.includes("-surface-")) {
85
- valid = false;
86
- warnings.push("Surface colors should not be used for text. Use --nys-color-text-* tokens.");
87
- alternatives.push("--nys-color-text", "--nys-color-text-weak", "--nys-color-text-weaker");
88
- }
89
- else if (varName.includes("-text-")) {
90
- recommendations.push("Pair with --nys-color-surface for optimal contrast on light backgrounds");
91
- recommendations.push("Pair with --nys-color-surface-reverse for dark backgrounds");
92
- }
93
- else if (varName.includes("-link-")) {
94
- recommendations.push("Ensure links are distinguishable from body text");
95
- recommendations.push("Include visible focus and hover states");
96
- }
97
- else if (varName.includes("-danger-") || varName.includes("-warning-")) {
98
- recommendations.push("Pair with corresponding icon for accessibility");
99
- }
100
- }
101
- else if (context === "background") {
102
- if (varName.includes("-text-")) {
103
- valid = false;
104
- warnings.push("Text colors should not be used for backgrounds. Use --nys-color-surface-* tokens.");
105
- alternatives.push("--nys-color-surface", "--nys-color-surface-raised", "--nys-color-surface-reverse");
106
- }
107
- else if (varName.includes("-surface-")) {
108
- if (varName.includes("-reverse")) {
109
- recommendations.push("Use --nys-color-text-reverse for text on dark surfaces");
110
- }
111
- else {
112
- recommendations.push("Use --nys-color-text for text on light surfaces");
113
- }
114
- }
115
- else if (varName.includes("-weak") &&
116
- (varName.includes("-success-") ||
117
- varName.includes("-info-") ||
118
- varName.includes("-warning-") ||
119
- varName.includes("-danger-"))) {
120
- recommendations.push("Use for alert/notification backgrounds");
121
- recommendations.push("Pair with corresponding -strong variant for text color");
122
- }
123
- }
124
- else if (context === "border") {
125
- if (varName.includes("-base-")) {
126
- recommendations.push("Good for borders and dividers");
127
- }
128
- else if (varName.includes("-focus")) {
129
- recommendations.push("Reserved for focus ring outlines");
130
- recommendations.push("Use 2px solid for visible focus indication");
131
- }
132
- else if (varName.includes("-theme-")) {
133
- recommendations.push("Use for branded accent borders");
134
- }
135
- }
136
- else if (context === "icon") {
137
- if (varName.includes("-ink-")) {
138
- recommendations.push("Primary choice for icons");
139
- if (varName.includes("-reverse")) {
140
- recommendations.push("Use on dark backgrounds");
141
- }
142
- }
143
- else if (varName.includes("-text-")) {
144
- recommendations.push("Icons can use text colors for consistency");
145
- }
146
- else if (varName.includes("-success-") ||
147
- varName.includes("-danger-") ||
148
- varName.includes("-warning-") ||
149
- varName.includes("-info-")) {
150
- recommendations.push("Use for status icons to reinforce meaning with color");
151
- }
152
- }
153
- // Find similar tokens as alternatives
154
- if (alternatives.length === 0) {
155
- const tokens = getCSSTokens();
156
- const category = token.category;
157
- const similar = tokens
158
- .filter((t) => t.category === category &&
159
- t.cssVariable !== varName &&
160
- ((context === "text" && t.cssVariable.includes("-text-")) ||
161
- (context === "background" && t.cssVariable.includes("-surface-")) ||
162
- (context === "border" && t.cssVariable.includes("-base-")) ||
163
- (context === "icon" && t.cssVariable.includes("-ink-"))))
164
- .slice(0, 3)
165
- .map((t) => t.cssVariable);
166
- alternatives.push(...similar);
167
- }
168
- return {
169
- valid,
170
- cssVariable: token.cssVariable,
171
- context,
172
- recommendations,
173
- alternatives,
174
- warnings: warnings.length > 0 ? warnings : undefined,
175
- };
176
- }
28
+ const TOKEN_TOOL_ANNOTATIONS = {
29
+ readOnlyHint: true,
30
+ destructiveHint: false,
31
+ idempotentHint: true,
32
+ openWorldHint: false,
33
+ };
177
34
  // ============================================================================
178
35
  // Tool Registration
179
36
  // ============================================================================
180
37
  export function registerTokenTools(server) {
181
38
  // -------------------------------------------------------------------------
182
- // get_tokens - Consolidated token retrieval (tokens, categories, or themes)
39
+ // find_tokens - Search or browse tokens (discovery)
183
40
  // -------------------------------------------------------------------------
184
- server.tool("get_tokens", "Get NYSDS design tokens, token categories, or agency themes", {
185
- output: z
186
- .enum(["tokens", "categories", "themes"])
187
- .default("tokens")
188
- .describe("What to return: 'tokens' for CSS variables, 'categories' for category list with counts, 'themes' for agency themes"),
189
- category: z
190
- .enum([
191
- "color",
192
- "font",
193
- "space",
194
- "size",
195
- "radius",
196
- "shadow",
197
- "border",
198
- "form",
199
- "gutter",
200
- "icon",
201
- ])
202
- .optional()
203
- .describe("Filter tokens by category (only applies when output='tokens')"),
204
- layer: z
205
- .enum(["primitive", "applied", "theme", "appearance"])
41
+ server.tool("find_tokens", "Search or browse NYSDS design tokens. Omit query to list all. Returns CSS variable names for use with var().", {
42
+ query: z
43
+ .string()
206
44
  .optional()
207
- .describe("Filter tokens by layer (only applies when output='tokens'). primitive = raw values, applied = semantic tokens, theme = agency theme tokens, appearance = light/dark mode"),
208
- includeResolvedValues: z
209
- .boolean()
210
- .default(false)
211
- .describe("Include resolved hex/rem values (only applies when output='tokens')"),
212
- includeDescriptions: z
213
- .boolean()
214
- .default(true)
215
- .describe("Include token descriptions (only applies when output='tokens')"),
216
- }, async ({ output, category, layer, includeResolvedValues, includeDescriptions, }) => {
217
- // Return categories
218
- if (output === "categories") {
219
- const categories = getCSSTokenCategories();
220
- const total = categories.reduce((sum, c) => sum + c.count, 0);
221
- return {
222
- content: [
223
- {
224
- type: "text",
225
- text: formatResponse({ categories, totalTokens: total }),
226
- },
227
- ],
228
- };
229
- }
230
- // Return themes
231
- if (output === "themes") {
232
- return {
233
- content: [
234
- {
235
- type: "text",
236
- text: formatResponse({
237
- themes: AGENCY_THEMES,
238
- usage: {
239
- htmlAttribute: '<html data-theme="admin">',
240
- cssClass: '<body class="nys-theme-admin">',
241
- cssCustomProperty: "Themes modify --nys-color-theme-* tokens",
242
- },
243
- totalThemes: AGENCY_THEMES.length,
244
- }),
245
- },
246
- ],
247
- };
248
- }
249
- // Return tokens (default)
45
+ .describe("Free-text search across token names and descriptions. Omit to list all."),
46
+ category: z.enum(CATEGORY_ENUM).optional().describe("Filter by category"),
47
+ include: z
48
+ .enum(["recommended", "primitive", "all"])
49
+ .default("recommended")
50
+ .describe("'recommended' (default) excludes color primitives; 'primitive' returns only primitives; 'all' returns everything"),
51
+ }, TOKEN_TOOL_ANNOTATIONS, async ({ query, category, include }) => {
52
+ // Pick token set based on include value
250
53
  let tokens;
251
- if (category) {
252
- tokens = getCSSTokensByCategory(category);
54
+ if (include === "primitive") {
55
+ tokens = getPrimitiveTokens();
253
56
  }
254
- else if (layer) {
255
- tokens = getCSSTokensByLayer(layer);
57
+ else if (include === "all") {
58
+ tokens = getCSSTokens();
256
59
  }
257
60
  else {
258
- tokens = getCSSTokens();
61
+ tokens = getRecommendedTokens();
259
62
  }
260
- // Format output based on options
261
- const formatted = tokens.map((t) => {
262
- const result = {
263
- cssVariable: t.cssVariable,
264
- cssValue: t.cssValue,
265
- };
266
- if (includeResolvedValues && t.resolvedValue) {
267
- result.resolvedValue = t.resolvedValue;
268
- }
269
- if (includeDescriptions && t.description) {
270
- result.description = t.description;
271
- }
272
- return result;
273
- });
274
- return {
275
- content: [
276
- {
277
- type: "text",
278
- text: formatResponse({ count: tokens.length, tokens: formatted }),
279
- },
280
- ],
281
- };
282
- });
283
- // -------------------------------------------------------------------------
284
- // find_tokens - Search tokens
285
- // -------------------------------------------------------------------------
286
- server.tool("find_tokens", "Search for design tokens by CSS variable name, value, or description", {
287
- query: z
288
- .string()
289
- .describe("Search query to match against CSS variable names, values, and descriptions"),
290
- category: z
291
- .enum([
292
- "color",
293
- "font",
294
- "space",
295
- "size",
296
- "radius",
297
- "shadow",
298
- "border",
299
- "form",
300
- "gutter",
301
- "icon",
302
- ])
303
- .optional()
304
- .describe("Optionally filter results to a specific category"),
305
- }, async ({ query, category }) => {
306
- let results = searchCSSTokens(query);
63
+ // Apply category filter
307
64
  if (category) {
308
- results = results.filter((t) => t.category.toLowerCase() === category.toLowerCase());
65
+ tokens = tokens.filter((t) => t.category.toLowerCase() === category.toLowerCase());
66
+ }
67
+ // Apply query filter (case-insensitive match on cssVariable or description)
68
+ if (query) {
69
+ const lowerQuery = query.toLowerCase();
70
+ tokens = tokens.filter((t) => t.cssVariable.toLowerCase().includes(lowerQuery) ||
71
+ t.description?.toLowerCase().includes(lowerQuery));
309
72
  }
310
- const formatted = results.map((t) => ({
73
+ const results = tokens.map((t) => ({
311
74
  cssVariable: t.cssVariable,
312
- cssValue: t.cssValue,
313
75
  description: t.description,
314
76
  category: t.category,
315
77
  }));
@@ -317,46 +79,35 @@ export function registerTokenTools(server) {
317
79
  content: [
318
80
  {
319
81
  type: "text",
320
- text: formatResponse({
321
- query,
322
- count: results.length,
323
- results: formatted,
324
- }),
82
+ text: JSON.stringify({ count: results.length, results }, null, 2),
325
83
  },
326
84
  ],
327
85
  };
328
86
  });
329
87
  // -------------------------------------------------------------------------
330
- // get_token_info - Get detailed token info with optional context validation
88
+ // get_token - Get full details for a specific token (deep dive)
331
89
  // -------------------------------------------------------------------------
332
- server.tool("get_token_info", "Get detailed information about a specific design token. Optionally validate usage in a specific context (text, background, border, icon).", {
90
+ server.tool("get_token", "Get full details for a specific design token including resolved values and alternatives.", {
333
91
  token: z
334
92
  .string()
335
93
  .describe('CSS variable name (e.g., "--nys-color-text" or "color-text")'),
336
- context: z
337
- .enum(["text", "background", "border", "icon"])
338
- .optional()
339
- .describe("Optional: validate token usage for a specific context. When provided, includes recommendations and warnings."),
340
- }, async ({ token, context }) => {
94
+ }, TOKEN_TOOL_ANNOTATIONS, async ({ token }) => {
341
95
  const tokenInfo = getCSSTokenByVariable(token);
342
96
  if (!tokenInfo) {
343
97
  return {
344
98
  content: [
345
99
  {
346
100
  type: "text",
347
- text: formatResponse({
101
+ text: JSON.stringify({
348
102
  error: `Token not found: ${token}`,
349
- suggestion: "Try searching with find_tokens to discover available tokens",
350
- }),
103
+ suggestion: "Use find_tokens to search for available tokens",
104
+ }, null, 2),
351
105
  },
352
106
  ],
107
+ isError: true,
353
108
  };
354
109
  }
355
- // Get dependents (tokens that use this one)
356
- const graph = buildTokenGraph();
357
- const node = graph.get(tokenInfo.cssVariable);
358
- const usedBy = node?.usedBy || [];
359
- // Base token info
110
+ const alternatives = getAlternativeTokens(tokenInfo);
360
111
  const result = {
361
112
  cssVariable: tokenInfo.cssVariable,
362
113
  cssValue: tokenInfo.cssValue,
@@ -364,30 +115,14 @@ export function registerTokenTools(server) {
364
115
  description: tokenInfo.description,
365
116
  category: tokenInfo.category,
366
117
  layer: tokenInfo.layer,
367
- usedBy: usedBy.length > 0 ? usedBy : undefined,
368
- usage: `var(${tokenInfo.cssVariable})`,
118
+ alternatives,
369
119
  resourceUri: "nysds://tokens",
370
120
  };
371
- // If context provided, add validation info
372
- if (context) {
373
- const validation = validateTokenInContext(tokenInfo, context);
374
- result.validation = {
375
- context,
376
- valid: validation.valid,
377
- recommendations: validation.recommendations.length > 0
378
- ? validation.recommendations
379
- : undefined,
380
- warnings: validation.warnings,
381
- alternatives: validation.alternatives.length > 0
382
- ? validation.alternatives
383
- : undefined,
384
- };
385
- }
386
121
  return {
387
122
  content: [
388
123
  {
389
124
  type: "text",
390
- text: formatResponse(result),
125
+ text: JSON.stringify(result, null, 2),
391
126
  },
392
127
  ],
393
128
  };
@@ -401,18 +136,7 @@ export function registerTokenTools(server) {
401
136
  .default("all")
402
137
  .describe("Filter the graph to specific token types"),
403
138
  category: z
404
- .enum([
405
- "color",
406
- "font",
407
- "space",
408
- "size",
409
- "radius",
410
- "shadow",
411
- "border",
412
- "form",
413
- "gutter",
414
- "icon",
415
- ])
139
+ .enum(CATEGORY_ENUM)
416
140
  .optional()
417
141
  .describe("Filter to a specific category"),
418
142
  limit: z
@@ -422,7 +146,7 @@ export function registerTokenTools(server) {
422
146
  .max(100)
423
147
  .default(20)
424
148
  .describe("Maximum number of tokens to return (default: 20)"),
425
- }, async ({ filter, category, limit }) => {
149
+ }, TOKEN_TOOL_ANNOTATIONS, async ({ filter, category, limit }) => {
426
150
  const graph = buildTokenGraph();
427
151
  const tokens = getCSSTokens();
428
152
  // Build lookup for categories
@@ -430,7 +154,8 @@ export function registerTokenTools(server) {
430
154
  for (const t of tokens) {
431
155
  tokenCategories.set(t.cssVariable, t.category);
432
156
  }
433
- let nodes = Array.from(graph.values());
157
+ const allNodes = Array.from(graph.values());
158
+ let nodes = allNodes;
434
159
  // Apply category filter
435
160
  if (category) {
436
161
  nodes = nodes.filter((n) => tokenCategories.get(n.cssVariable) === category);
@@ -448,8 +173,6 @@ export function registerTokenTools(server) {
448
173
  }
449
174
  // Apply limit
450
175
  nodes = nodes.slice(0, limit);
451
- // Calculate stats
452
- const allNodes = Array.from(graph.values());
453
176
  const stats = {
454
177
  totalTokens: allNodes.length,
455
178
  aliasTokens: allNodes.filter((n) => n.referencesVariable).length,
@@ -467,7 +190,7 @@ export function registerTokenTools(server) {
467
190
  content: [
468
191
  {
469
192
  type: "text",
470
- text: formatResponse({
193
+ text: JSON.stringify({
471
194
  filter,
472
195
  category: category || "all",
473
196
  nodes: nodes.map((n) => ({
@@ -476,7 +199,7 @@ export function registerTokenTools(server) {
476
199
  usedBy: n.usedBy.length > 0 ? n.usedBy : undefined,
477
200
  })),
478
201
  stats,
479
- }),
202
+ }, null, 2),
480
203
  },
481
204
  ],
482
205
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nysds/mcp-server",
3
- "version": "1.18.0",
3
+ "version": "1.18.3",
4
4
  "description": "MCP server for the New York State Design System. Exposes components, tokens, and documentation to AI assistants.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,8 +30,8 @@
30
30
  "zod": "^3.25"
31
31
  },
32
32
  "devDependencies": {
33
- "@nysds/styles": "^1.18.0",
34
- "@nysds/tokens": "^1.18.0",
33
+ "@nysds/styles": "^1.18.3",
34
+ "@nysds/tokens": "^1.18.3",
35
35
  "@types/node": "^22.0.0",
36
36
  "typescript": "^5.9.3"
37
37
  },