design-constraint-validator 2.0.1 → 2.2.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.
- package/README.md +89 -23
- package/cli/commands/build.d.ts.map +1 -1
- package/cli/commands/build.js +32 -24
- package/cli/commands/build.ts +26 -17
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +35 -18
- package/cli/commands/graph.ts +30 -17
- package/cli/commands/patch-apply.d.ts.map +1 -1
- package/cli/commands/patch-apply.js +4 -1
- package/cli/commands/patch-apply.ts +4 -1
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +18 -19
- package/cli/commands/set.ts +19 -19
- package/cli/commands/utils.d.ts +1 -0
- package/cli/commands/utils.d.ts.map +1 -1
- package/cli/commands/utils.js +20 -1
- package/cli/commands/utils.ts +23 -1
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +45 -23
- package/cli/commands/validate.ts +47 -26
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +22 -10
- package/cli/commands/why.ts +20 -9
- package/cli/config-schema.d.ts +171 -166
- package/cli/config-schema.d.ts.map +1 -1
- package/cli/config-schema.js +29 -7
- package/cli/config-schema.ts +31 -7
- package/cli/config.d.ts.map +1 -1
- package/cli/config.js +8 -2
- package/cli/config.ts +8 -2
- package/cli/constraint-registry.d.ts +16 -0
- package/cli/constraint-registry.d.ts.map +1 -1
- package/cli/constraint-registry.js +115 -44
- package/cli/constraint-registry.ts +118 -47
- package/cli/cross-axis-loader.d.ts +62 -0
- package/cli/cross-axis-loader.d.ts.map +1 -1
- package/cli/cross-axis-loader.js +186 -31
- package/cli/cross-axis-loader.ts +199 -24
- package/cli/dcv.js +31 -25
- package/cli/dcv.ts +31 -21
- package/cli/json-output.d.ts +3 -1
- package/cli/json-output.d.ts.map +1 -1
- package/cli/json-output.js +11 -4
- package/cli/json-output.ts +13 -4
- package/cli/types.d.ts +21 -9
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +25 -10
- package/cli/validate-api.d.ts +40 -0
- package/cli/validate-api.d.ts.map +1 -0
- package/cli/validate-api.js +90 -0
- package/cli/validate-api.ts +131 -0
- package/core/breakpoints.d.ts +8 -2
- package/core/breakpoints.d.ts.map +1 -1
- package/core/breakpoints.js +24 -3
- package/core/breakpoints.ts +22 -3
- package/core/color.js +4 -4
- package/core/color.ts +4 -4
- package/core/constraints/cross-axis.d.ts.map +1 -1
- package/core/constraints/cross-axis.js +37 -9
- package/core/constraints/cross-axis.ts +37 -9
- package/core/constraints/monotonic-lightness.d.ts.map +1 -1
- package/core/constraints/monotonic-lightness.js +9 -5
- package/core/constraints/monotonic-lightness.ts +9 -4
- package/core/constraints/monotonic.d.ts.map +1 -1
- package/core/constraints/monotonic.js +32 -8
- package/core/constraints/monotonic.ts +29 -8
- package/core/constraints/threshold.d.ts.map +1 -1
- package/core/constraints/threshold.js +24 -4
- package/core/constraints/threshold.ts +23 -4
- package/core/constraints/wcag.d.ts.map +1 -1
- package/core/constraints/wcag.js +7 -1
- package/core/constraints/wcag.ts +7 -1
- package/core/dtcg.d.ts +38 -0
- package/core/dtcg.d.ts.map +1 -0
- package/core/dtcg.js +88 -0
- package/core/dtcg.ts +102 -0
- package/core/engine.d.ts +6 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.ts +7 -0
- package/core/flatten.d.ts +5 -3
- package/core/flatten.d.ts.map +1 -1
- package/core/flatten.js +32 -10
- package/core/flatten.ts +48 -16
- package/core/image-export.d.ts.map +1 -1
- package/core/image-export.js +10 -7
- package/core/image-export.ts +9 -6
- package/core/index.d.ts +2 -0
- package/core/index.d.ts.map +1 -1
- package/core/index.js +4 -0
- package/core/index.ts +6 -0
- package/core/poset.d.ts +6 -1
- package/core/poset.d.ts.map +1 -1
- package/core/poset.js +7 -2
- package/core/poset.ts +7 -2
- package/core/why.d.ts +1 -1
- package/core/why.d.ts.map +1 -1
- package/core/why.ts +1 -1
- package/mcp/contracts.d.ts +1561 -0
- package/mcp/contracts.d.ts.map +1 -0
- package/mcp/contracts.js +74 -0
- package/mcp/contracts.ts +105 -0
- package/mcp/index.d.ts +11 -0
- package/mcp/index.d.ts.map +1 -0
- package/mcp/index.js +35 -0
- package/mcp/index.ts +97 -0
- package/mcp/insights.d.ts +94 -0
- package/mcp/insights.d.ts.map +1 -0
- package/mcp/insights.js +445 -0
- package/mcp/insights.ts +541 -0
- package/mcp/tools.d.ts +63 -0
- package/mcp/tools.d.ts.map +1 -0
- package/mcp/tools.js +299 -0
- package/mcp/tools.ts +431 -0
- package/package.json +36 -26
- package/server.json +21 -0
- package/cli/constraints-loader.d.ts.map +0 -1
- package/cli/engine-helpers.d.ts.map +0 -1
- package/core/cross-axis-config.d.ts.map +0 -1
package/mcp/tools.ts
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import type { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import { suggestIds } from '../core/cli-format.js';
|
|
7
|
+
import { flattenTokens, type TokenNode, type FlatToken } from '../core/flatten.js';
|
|
8
|
+
import { explain as explainWhy, type WhyReport } from '../core/why.js';
|
|
9
|
+
import { Engine } from '../core/engine.js';
|
|
10
|
+
import { ConstraintsSchema } from '../cli/config-schema.js';
|
|
11
|
+
import { loadConfig } from '../cli/config.js';
|
|
12
|
+
import { discoverConstraints } from '../cli/constraint-registry.js';
|
|
13
|
+
import { validate, type ValidateResult } from '../cli/validate-api.js';
|
|
14
|
+
import type { DcvConfig } from '../cli/types.js';
|
|
15
|
+
import type { Breakpoint } from '../core/breakpoints.js';
|
|
16
|
+
import {
|
|
17
|
+
describeConstraints,
|
|
18
|
+
explain as explainInsight,
|
|
19
|
+
suggestFix as suggestFixInsight,
|
|
20
|
+
InsightError,
|
|
21
|
+
type ConstraintDescriptor,
|
|
22
|
+
type ExplainResult,
|
|
23
|
+
type SuggestResult,
|
|
24
|
+
type ValueResolver,
|
|
25
|
+
} from './insights.js';
|
|
26
|
+
import type {
|
|
27
|
+
JsonObject,
|
|
28
|
+
ValidateToolInput,
|
|
29
|
+
WhyToolInput,
|
|
30
|
+
GraphToolInput,
|
|
31
|
+
ListConstraintsToolInput,
|
|
32
|
+
ExplainToolInput,
|
|
33
|
+
SuggestFixToolInput,
|
|
34
|
+
} from './contracts.js';
|
|
35
|
+
import {
|
|
36
|
+
graphInputShape,
|
|
37
|
+
validateInputShape,
|
|
38
|
+
whyInputShape,
|
|
39
|
+
listConstraintsInputShape,
|
|
40
|
+
explainInputShape,
|
|
41
|
+
suggestFixInputShape,
|
|
42
|
+
} from './contracts.js';
|
|
43
|
+
|
|
44
|
+
export type DcvMcpToolName = 'validate' | 'why' | 'graph' | 'list-constraints' | 'explain' | 'suggest-fix';
|
|
45
|
+
|
|
46
|
+
export interface ToolFailure {
|
|
47
|
+
ok: false;
|
|
48
|
+
tool: DcvMcpToolName;
|
|
49
|
+
error: {
|
|
50
|
+
code: string;
|
|
51
|
+
message: string;
|
|
52
|
+
details?: Record<string, unknown>;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ToolResponse<T extends { ok: boolean } & object> = ({ tool: DcvMcpToolName } & T) | ToolFailure;
|
|
57
|
+
|
|
58
|
+
export class ToolExecutionError extends Error {
|
|
59
|
+
readonly code: string;
|
|
60
|
+
readonly details?: Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
constructor(code: string, message: string, details?: Record<string, unknown>) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.code = code;
|
|
65
|
+
this.details = details;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface GraphToolResult {
|
|
70
|
+
ok: true;
|
|
71
|
+
nodes: string[];
|
|
72
|
+
edges: Array<[string, string]>;
|
|
73
|
+
meta: {
|
|
74
|
+
nodeCount: number;
|
|
75
|
+
edgeCount: number;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type WhyToolResult = { ok: true } & WhyReport;
|
|
80
|
+
|
|
81
|
+
interface TokenInput {
|
|
82
|
+
tokens?: JsonObject;
|
|
83
|
+
tokensPath?: string;
|
|
84
|
+
constraints?: unknown;
|
|
85
|
+
configPath?: string;
|
|
86
|
+
constraintsDir?: string;
|
|
87
|
+
breakpoint?: Breakpoint;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface ToolDefinition<TInput, TResult extends { ok: boolean } & object> {
|
|
91
|
+
name: DcvMcpToolName;
|
|
92
|
+
description: string;
|
|
93
|
+
inputSchema: Record<string, z.ZodTypeAny>;
|
|
94
|
+
handler: (input: TInput) => Promise<ToolResponse<TResult>> | ToolResponse<TResult>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function errorMessage(error: unknown): string {
|
|
98
|
+
return error instanceof Error ? error.message : 'Unknown tool failure';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function toFailure(tool: DcvMcpToolName, error: unknown): ToolFailure {
|
|
102
|
+
if (error instanceof ToolExecutionError) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
tool,
|
|
106
|
+
error: {
|
|
107
|
+
code: error.code,
|
|
108
|
+
message: error.message,
|
|
109
|
+
...(error.details ? { details: error.details } : {}),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Pure derivation errors (bad input, unsupported rule) carry their own code.
|
|
115
|
+
if (error instanceof InsightError) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
tool,
|
|
119
|
+
error: {
|
|
120
|
+
code: error.code,
|
|
121
|
+
message: error.message,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
tool,
|
|
129
|
+
error: {
|
|
130
|
+
code: 'tool_execution_failed',
|
|
131
|
+
message: errorMessage(error),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function executeTool<T extends { ok: boolean } & object>(
|
|
137
|
+
tool: DcvMcpToolName,
|
|
138
|
+
fn: () => Promise<T> | T,
|
|
139
|
+
): Promise<ToolResponse<T>> {
|
|
140
|
+
try {
|
|
141
|
+
return {
|
|
142
|
+
tool,
|
|
143
|
+
...(await fn()),
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return toFailure(tool, error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isToolFailure(result: ToolResponse<{ ok: boolean }>): result is ToolFailure {
|
|
151
|
+
return result.ok === false && 'error' in result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function responseToContent(result: ToolResponse<{ ok: boolean }>): string {
|
|
155
|
+
return JSON.stringify(result, null, 2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseJsonFile(filePath: string): unknown {
|
|
159
|
+
if (!fs.existsSync(filePath)) {
|
|
160
|
+
throw new ToolExecutionError('tokens_not_found', `Tokens file not found: ${filePath}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new ToolExecutionError('invalid_tokens', `Tokens file is not valid JSON: ${filePath}`, {
|
|
167
|
+
cause: errorMessage(error),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function asJsonObject(value: unknown, label: string): JsonObject {
|
|
173
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
174
|
+
return value as JsonObject;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw new ToolExecutionError('invalid_input', `${label} must be a JSON object.`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function zodIssueMessages(error: z.ZodError): string[] {
|
|
181
|
+
return error.issues.map((issue) => {
|
|
182
|
+
const path = issue.path.length > 0 ? `.${issue.path.join('.')}` : '';
|
|
183
|
+
return `constraints${path}: ${issue.message}`;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveTokens(input: TokenInput): TokenNode {
|
|
188
|
+
if (input.tokens !== undefined) {
|
|
189
|
+
// TASK-017: validate inline tokens at the handler boundary. A direct caller
|
|
190
|
+
// (not going through the MCP SDK's schema check) could otherwise pass an
|
|
191
|
+
// array/null/scalar that silently flattened to an empty, passing set.
|
|
192
|
+
return asJsonObject(input.tokens, 'tokens') as unknown as TokenNode;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (input.tokensPath !== undefined) {
|
|
196
|
+
return asJsonObject(parseJsonFile(input.tokensPath), 'tokensPath contents') as unknown as TokenNode;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new ToolExecutionError('invalid_input', 'Provide either tokens or tokensPath.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function constraints(input: TokenInput): DcvConfig['constraints'] | undefined {
|
|
203
|
+
if (input.constraints === undefined) return undefined;
|
|
204
|
+
// TASK-017: reject malformed inline constraints at the boundary.
|
|
205
|
+
const object = asJsonObject(input.constraints, 'constraints');
|
|
206
|
+
const parsed = ConstraintsSchema.safeParse(object);
|
|
207
|
+
if (!parsed.success) {
|
|
208
|
+
throw new ToolExecutionError('invalid_input', 'constraints must match DCV constraint config schema.', {
|
|
209
|
+
issues: zodIssueMessages(parsed.error),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return parsed.data;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function validateTool(input: ValidateToolInput): Promise<ToolResponse<ValidateResult>> {
|
|
216
|
+
return executeTool('validate', () => {
|
|
217
|
+
if (input.tokens === undefined && input.tokensPath === undefined) {
|
|
218
|
+
throw new ToolExecutionError('invalid_input', 'Provide either tokens or tokensPath.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return validate({
|
|
222
|
+
...(input.tokens !== undefined ? { tokens: asJsonObject(input.tokens, 'tokens') as unknown as TokenNode } : {}),
|
|
223
|
+
...(input.tokens === undefined && input.tokensPath !== undefined ? { tokensPath: input.tokensPath } : {}),
|
|
224
|
+
...(input.constraints !== undefined ? { constraints: constraints(input) } : {}),
|
|
225
|
+
...(input.configPath !== undefined ? { configPath: input.configPath } : {}),
|
|
226
|
+
...(input.constraintsDir !== undefined ? { constraintsDir: input.constraintsDir } : {}),
|
|
227
|
+
...(input.breakpoint !== undefined ? { breakpoint: input.breakpoint } : {}),
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function whyTool(input: WhyToolInput): Promise<ToolResponse<WhyToolResult>> {
|
|
233
|
+
return executeTool('why', () => {
|
|
234
|
+
const { flat, edges } = flattenTokens(resolveTokens(input));
|
|
235
|
+
if (!Object.prototype.hasOwnProperty.call(flat, input.tokenId)) {
|
|
236
|
+
throw new ToolExecutionError('unknown_token', `Unknown token id: ${input.tokenId}`, {
|
|
237
|
+
tokenId: input.tokenId,
|
|
238
|
+
suggestions: suggestIds(input.tokenId, Object.keys(flat)).map((suggestion) => suggestion.id),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
ok: true as const,
|
|
244
|
+
...explainWhy(input.tokenId, flat, edges),
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function graphTool(input: GraphToolInput): Promise<ToolResponse<GraphToolResult>> {
|
|
250
|
+
return executeTool('graph', () => {
|
|
251
|
+
const { flat, edges } = flattenTokens(resolveTokens(input));
|
|
252
|
+
const nodes = Object.keys(flat).sort();
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
ok: true as const,
|
|
256
|
+
nodes,
|
|
257
|
+
edges,
|
|
258
|
+
meta: {
|
|
259
|
+
nodeCount: nodes.length,
|
|
260
|
+
edgeCount: edges.length,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface ListConstraintsResult {
|
|
267
|
+
ok: true;
|
|
268
|
+
constraints: ConstraintDescriptor[];
|
|
269
|
+
meta: { count: number };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Resolve the constraint config the same way validate() does: inline → configPath
|
|
273
|
+
* → discovered cwd config. */
|
|
274
|
+
function resolveConstraintsConfig(input: TokenInput): DcvConfig {
|
|
275
|
+
if (input.constraints !== undefined) {
|
|
276
|
+
return { constraints: constraints(input) };
|
|
277
|
+
}
|
|
278
|
+
const res = loadConfig(input.configPath);
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
throw new ToolExecutionError('invalid_config', res.error);
|
|
281
|
+
}
|
|
282
|
+
return res.value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface DerivedContext {
|
|
286
|
+
descriptors: ConstraintDescriptor[];
|
|
287
|
+
getValue: ValueResolver;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Flatten tokens, build an engine for value resolution, and discover the active
|
|
291
|
+
* constraint sources — the shared substrate for the read-only insight tools. */
|
|
292
|
+
function deriveContext(input: TokenInput): DerivedContext {
|
|
293
|
+
const tokens = resolveTokens(input);
|
|
294
|
+
const config = resolveConstraintsConfig(input);
|
|
295
|
+
|
|
296
|
+
const { flat, edges } = flattenTokens(tokens);
|
|
297
|
+
const init: Record<string, string | number> = {};
|
|
298
|
+
for (const t of Object.values(flat)) {
|
|
299
|
+
init[(t as FlatToken).id] = (t as FlatToken).value;
|
|
300
|
+
}
|
|
301
|
+
const engine = new Engine(init, edges);
|
|
302
|
+
const knownIds = new Set(Object.keys(init));
|
|
303
|
+
|
|
304
|
+
const sources = discoverConstraints({
|
|
305
|
+
config,
|
|
306
|
+
bp: input.breakpoint,
|
|
307
|
+
constraintsDir: input.constraintsDir ?? 'themes',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
descriptors: describeConstraints(sources),
|
|
312
|
+
// Token ids resolve to their value; anything else (a literal backdrop color)
|
|
313
|
+
// passes through, mirroring the WCAG plugin's resolveColor.
|
|
314
|
+
getValue: (idOrLiteral) => (knownIds.has(idOrLiteral) ? String(engine.get(idOrLiteral)) : idOrLiteral),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** explain / suggest-fix accept a full violation OR loose ruleId + nodes. */
|
|
319
|
+
function resolveInsightTarget(
|
|
320
|
+
input: ExplainToolInput | SuggestFixToolInput,
|
|
321
|
+
): { ruleId: string; nodes: string[]; context?: Record<string, unknown> } {
|
|
322
|
+
if (input.violation) {
|
|
323
|
+
return {
|
|
324
|
+
ruleId: input.violation.ruleId,
|
|
325
|
+
nodes: input.violation.nodes ?? [],
|
|
326
|
+
context: input.violation.context as Record<string, unknown> | undefined,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (input.ruleId) {
|
|
330
|
+
return {
|
|
331
|
+
ruleId: input.ruleId,
|
|
332
|
+
nodes: input.nodes ?? [],
|
|
333
|
+
context: input.context as Record<string, unknown> | undefined,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
throw new ToolExecutionError('invalid_input', 'Provide either a violation object or ruleId (with nodes).');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function listConstraintsTool(input: ListConstraintsToolInput): Promise<ToolResponse<ListConstraintsResult>> {
|
|
340
|
+
return executeTool('list-constraints', () => {
|
|
341
|
+
const { descriptors } = deriveContext(input);
|
|
342
|
+
return { ok: true as const, constraints: descriptors, meta: { count: descriptors.length } };
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function explainTool(input: ExplainToolInput): Promise<ToolResponse<ExplainResult>> {
|
|
347
|
+
return executeTool('explain', () => {
|
|
348
|
+
const { descriptors, getValue } = deriveContext(input);
|
|
349
|
+
const target = resolveInsightTarget(input);
|
|
350
|
+
return explainInsight({ ...target, getValue, descriptors });
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function suggestFixTool(input: SuggestFixToolInput): Promise<ToolResponse<SuggestResult>> {
|
|
355
|
+
return executeTool('suggest-fix', () => {
|
|
356
|
+
const { descriptors, getValue } = deriveContext(input);
|
|
357
|
+
const target = resolveInsightTarget(input);
|
|
358
|
+
return suggestFixInsight({ ...target, getValue, descriptors, target: input.target });
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export const dcvMcpTools: Array<
|
|
363
|
+
| ToolDefinition<ValidateToolInput, ValidateResult>
|
|
364
|
+
| ToolDefinition<WhyToolInput, WhyToolResult>
|
|
365
|
+
| ToolDefinition<GraphToolInput, GraphToolResult>
|
|
366
|
+
| ToolDefinition<ListConstraintsToolInput, ListConstraintsResult>
|
|
367
|
+
| ToolDefinition<ExplainToolInput, ExplainResult>
|
|
368
|
+
| ToolDefinition<SuggestFixToolInput, SuggestResult>
|
|
369
|
+
> = [
|
|
370
|
+
{
|
|
371
|
+
name: 'validate',
|
|
372
|
+
description: 'Validate DTCG-style design tokens against DCV constraints and return structured violations.',
|
|
373
|
+
inputSchema: validateInputShape,
|
|
374
|
+
handler: validateTool,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'why',
|
|
378
|
+
description: 'Explain token provenance, aliases, immediate dependencies, dependents, and alias chain.',
|
|
379
|
+
inputSchema: whyInputShape,
|
|
380
|
+
handler: whyTool,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: 'graph',
|
|
384
|
+
description: 'Return the token dependency graph as nodes and directed edges.',
|
|
385
|
+
inputSchema: graphInputShape,
|
|
386
|
+
handler: graphTool,
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: 'list-constraints',
|
|
390
|
+
description: 'List the active constraints (WCAG pairs, thresholds, order/lightness scales, cross-axis) for a token set/config. Read-only.',
|
|
391
|
+
inputSchema: listConstraintsInputShape,
|
|
392
|
+
handler: listConstraintsTool,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'explain',
|
|
396
|
+
description: 'Explain a validation violation (WCAG, threshold, monotonic) in plain English plus machine-readable facts. Read-only.',
|
|
397
|
+
inputSchema: explainInputShape,
|
|
398
|
+
handler: explainTool,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: 'suggest-fix',
|
|
402
|
+
description: 'Compute a verified satisfying value for a violation without writing it (WCAG color, threshold/monotonic boundary). Read-only.',
|
|
403
|
+
inputSchema: suggestFixInputShape,
|
|
404
|
+
handler: suggestFixTool,
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
export function registerDcvMcpTools(server: McpServer): void {
|
|
409
|
+
for (const tool of dcvMcpTools) {
|
|
410
|
+
server.registerTool(
|
|
411
|
+
tool.name,
|
|
412
|
+
{
|
|
413
|
+
description: tool.description,
|
|
414
|
+
inputSchema: tool.inputSchema,
|
|
415
|
+
},
|
|
416
|
+
async (input): Promise<CallToolResult> => {
|
|
417
|
+
const result = await tool.handler(input as never);
|
|
418
|
+
return {
|
|
419
|
+
content: [
|
|
420
|
+
{
|
|
421
|
+
type: 'text',
|
|
422
|
+
text: responseToContent(result as ToolResponse<{ ok: boolean }>),
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
structuredContent: result as unknown as Record<string, unknown>,
|
|
426
|
+
...(isToolFailure(result as ToolResponse<{ ok: boolean }>) ? { isError: true } : {}),
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "design-constraint-validator",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Mathematical constraint validator for design systems — ensuring consistency, accessibility, and logical coherence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -14,30 +14,37 @@
|
|
|
14
14
|
"import": "./core/index.js"
|
|
15
15
|
},
|
|
16
16
|
"./core/*": "./core/*",
|
|
17
|
-
"./cli/*": "./cli/*"
|
|
17
|
+
"./cli/*": "./cli/*",
|
|
18
|
+
"./mcp": {
|
|
19
|
+
"types": "./mcp/index.d.ts",
|
|
20
|
+
"import": "./mcp/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./mcp/*": "./mcp/*"
|
|
18
23
|
},
|
|
19
24
|
"bin": {
|
|
20
25
|
"dcv": "./cli/index.js",
|
|
21
|
-
"design-constraint-validator": "./cli/index.js"
|
|
26
|
+
"design-constraint-validator": "./cli/index.js",
|
|
27
|
+
"dcv-mcp": "./mcp/index.js"
|
|
22
28
|
},
|
|
29
|
+
"mcpName": "io.github.cseperkepapp/design-constraint-validator",
|
|
23
30
|
"scripts": {
|
|
24
|
-
"test": "vitest run",
|
|
25
|
-
"test:watch": "vitest",
|
|
31
|
+
"test": "vitest run --exclude \"**/*.test.js\"",
|
|
32
|
+
"test:watch": "vitest --exclude \"**/*.test.js\"",
|
|
26
33
|
"typecheck": "tsc --noEmit",
|
|
27
34
|
"build": "tsc",
|
|
28
35
|
"lint": "eslint . --ext .js,.ts,.mjs,.cjs",
|
|
29
36
|
"format": "prettier -w .",
|
|
30
|
-
"check": "npm run typecheck && npm run lint && npm test",
|
|
31
|
-
"prepublishOnly": "npm run check
|
|
37
|
+
"check": "npm run typecheck && npm run lint && npm run build && npm test",
|
|
38
|
+
"prepublishOnly": "npm run check",
|
|
32
39
|
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
|
|
33
40
|
"release:notes": "npm run changelog && git add CHANGELOG.md && git commit -m \"docs(changelog): update\"",
|
|
34
|
-
"release:patch": "npm version patch && git push && git push --tags && echo '\n
|
|
35
|
-
"release:minor": "npm version minor && git push && git push --tags && echo '\n
|
|
36
|
-
"release:major": "npm version major && git push && git push --tags && echo '\n
|
|
37
|
-
"demo": "
|
|
38
|
-
"demo:build": "
|
|
39
|
-
"demo:v2": "
|
|
40
|
-
"demo:v2:build": "
|
|
41
|
+
"release:patch": "npm version patch && git push && git push --tags && echo '\n✅ Tag pushed. CI (.github/workflows/publish.yml) will build, publish, and verify the version on npm — no manual npm publish needed. Watch the Actions run.'",
|
|
42
|
+
"release:minor": "npm version minor && git push && git push --tags && echo '\n✅ Tag pushed. CI (.github/workflows/publish.yml) will build, publish, and verify the version on npm — no manual npm publish needed. Watch the Actions run.'",
|
|
43
|
+
"release:major": "npm version major && git push && git push --tags && echo '\n✅ Tag pushed. CI (.github/workflows/publish.yml) will build, publish, and verify the version on npm — no manual npm publish needed. Watch the Actions run.'",
|
|
44
|
+
"demo": "node -e \"const fs=require('node:fs');const cp=require('node:child_process');const dir='../designLab-DEMO';if(!fs.existsSync(dir)){console.error('Demo repo not found at ../designLab-DEMO. Clone it next to this repo to run npm run demo.');process.exit(1)}const r=cp.spawnSync('npm',['run','dev'],{cwd:dir,stdio:'inherit',shell:true});process.exit(r.status===null?1:r.status)\"",
|
|
45
|
+
"demo:build": "node -e \"const fs=require('node:fs');const cp=require('node:child_process');const dir='../designLab-DEMO';if(!fs.existsSync(dir)){console.error('Demo repo not found at ../designLab-DEMO. Clone it next to this repo to run npm run demo:build.');process.exit(1)}const r=cp.spawnSync('npm',['run','build'],{cwd:dir,stdio:'inherit',shell:true});process.exit(r.status===null?1:r.status)\"",
|
|
46
|
+
"demo:v2": "node -e \"const fs=require('node:fs');const cp=require('node:child_process');const dir='designLab-v2';if(!fs.existsSync(dir)){console.error('Demo repo not found at designLab-v2. Add it inside this repo to run npm run demo:v2.');process.exit(1)}const r=cp.spawnSync('npm',['run','dev'],{cwd:dir,stdio:'inherit',shell:true});process.exit(r.status===null?1:r.status)\"",
|
|
47
|
+
"demo:v2:build": "node -e \"const fs=require('node:fs');const cp=require('node:child_process');const dir='designLab-v2';if(!fs.existsSync(dir)){console.error('Demo repo not found at designLab-v2. Add it inside this repo to run npm run demo:v2:build.');process.exit(1)}const r=cp.spawnSync('npm',['run','build'],{cwd:dir,stdio:'inherit',shell:true});process.exit(r.status===null?1:r.status)\""
|
|
41
48
|
},
|
|
42
49
|
"keywords": [
|
|
43
50
|
"design-constraints",
|
|
@@ -67,29 +74,32 @@
|
|
|
67
74
|
"access": "public"
|
|
68
75
|
},
|
|
69
76
|
"dependencies": {
|
|
70
|
-
"
|
|
71
|
-
"yargs": "^17.
|
|
72
|
-
"zod": "^3.
|
|
77
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
78
|
+
"yargs": "^17.7.2",
|
|
79
|
+
"zod": "^3.25.76"
|
|
73
80
|
},
|
|
74
81
|
"devDependencies": {
|
|
75
|
-
"@types/node": "^22.
|
|
76
|
-
"@types/yargs": "^17.0.
|
|
77
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
78
|
-
"@typescript-eslint/parser": "^8.
|
|
82
|
+
"@types/node": "^22.19.21",
|
|
83
|
+
"@types/yargs": "^17.0.35",
|
|
84
|
+
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
|
85
|
+
"@typescript-eslint/parser": "^8.61.0",
|
|
79
86
|
"conventional-changelog-cli": "^2.2.2",
|
|
80
|
-
"eslint": "^9.
|
|
87
|
+
"eslint": "^9.39.4",
|
|
81
88
|
"eslint-config-prettier": "^9.0.0",
|
|
89
|
+
"glob": "^11.1.0",
|
|
82
90
|
"pdf-parse": "^2.4.5",
|
|
83
|
-
"prettier": "^3.
|
|
84
|
-
"tsx": "^4.
|
|
85
|
-
"typescript": "^5.
|
|
86
|
-
"vitest": "^3.
|
|
91
|
+
"prettier": "^3.8.4",
|
|
92
|
+
"tsx": "^4.22.4",
|
|
93
|
+
"typescript": "^5.9.3",
|
|
94
|
+
"vitest": "^3.2.6"
|
|
87
95
|
},
|
|
88
96
|
"files": [
|
|
89
97
|
"cli/",
|
|
90
98
|
"core/",
|
|
99
|
+
"mcp/",
|
|
91
100
|
"adapters/",
|
|
92
101
|
"themes/",
|
|
102
|
+
"server.json",
|
|
93
103
|
"LICENSE",
|
|
94
104
|
"README.md"
|
|
95
105
|
]
|
package/server.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.cseperkepapp/design-constraint-validator",
|
|
4
|
+
"title": "Design Constraint Validator",
|
|
5
|
+
"description": "Validate design tokens for accessibility, scales, and design-system constraint consistency.",
|
|
6
|
+
"version": "2.2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/CseperkePapp/design-constraint-validator",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "design-constraint-validator",
|
|
15
|
+
"version": "2.2.0",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constraints-loader.d.ts","sourceRoot":"","sources":["constraints-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,KAAK,iBAAiB,GAAG;IACvB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,EAAE,CAAC,EAAE,UAAU,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAkDtF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"engine-helpers.d.ts","sourceRoot":"","sources":["engine-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAiB,KAAK,SAAS,EAAkB,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAI3C,OAAO,EAA8B,wBAAwB,EAAE,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAC/G,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAgE5C;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,SAAS,EAAE,MAAM,GAAE,SAAc,GAAG,MAAM,CAQlF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,UAAU,GAAG,SAAS,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM,CAgBnH;AAED,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cross-axis-config.d.ts","sourceRoot":"","sources":["cross-axis-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAUH;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,0CA2HnD"}
|