@expressivecss/mcp-server 0.2.2 → 0.3.1
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/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +91 -0
- package/README.md +485 -3
- package/capability-roadmap.json +1908 -221
- package/component-decisions.json +4406 -667
- package/component-guides.json +158 -42
- package/contract.json +6 -4
- package/package.json +2 -1
- package/semantics-data.json +601 -3
- package/server.js +488 -46
- package/smoke.mjs +1238 -3
package/server.js
CHANGED
|
@@ -5,10 +5,11 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { createHash, randomUUID } from 'node:crypto';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
|
-
import {
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
9
|
import * as z from 'zod/v4';
|
|
10
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import { McpError, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
13
|
import { resolveExpressiveVersion } from './scripts/resolve-version.mjs';
|
|
13
14
|
|
|
14
15
|
const SERVER_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -18,6 +19,14 @@ const SERVER_VERSION = JSON.parse(readFileSync(path.join(SERVER_DIR, 'package.js
|
|
|
18
19
|
const CAPABILITIES_BY_SLUG = new Map(CAPABILITY_ROADMAP.entries.map((entry) => [entry.slug, entry]));
|
|
19
20
|
const COMPONENT_DECISIONS_BY_SLUG = new Map(COMPONENT_DECISIONS.components.map((entry) => [entry.slug, entry]));
|
|
20
21
|
const DEFAULT_MAX_COMPONENT_RESPONSE_CHARS = 24_000;
|
|
22
|
+
const GUIDANCE_TOOLS = new Set(['setup_expert', 'creative_director', 'page_architect', 'page_arcjitect', 'component_syntax_expert', 'component_catalog']);
|
|
23
|
+
const RESPONSE_BUDGET_SETTING = 'EXPRESSIVECSS_MCP_MAX_RESPONSE_BYTES';
|
|
24
|
+
function configuredResponseBudget(value = '65536') {
|
|
25
|
+
if (!/^\d+$/u.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1) {
|
|
26
|
+
throw new Error(`${RESPONSE_BUDGET_SETTING} must be a positive safe integer in decimal bytes.`);
|
|
27
|
+
}
|
|
28
|
+
return Number(value);
|
|
29
|
+
}
|
|
21
30
|
const DEFAULT_QA_MAX_FILES = 300;
|
|
22
31
|
const DEFAULT_QA_MAX_MB = 2;
|
|
23
32
|
const DEFAULT_QA_MAX_TOTAL_MB = 16;
|
|
@@ -64,6 +73,7 @@ function configuredScripts(value) {
|
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
const SETTINGS = {
|
|
76
|
+
maxResponseBytes: configuredResponseBudget(process.env[RESPONSE_BUDGET_SETTING]),
|
|
67
77
|
maxComponentResponseChars: Number(process.env.EXPRESSIVECSS_MCP_MAX_COMPONENT_RESPONSE_CHARS || DEFAULT_MAX_COMPONENT_RESPONSE_CHARS),
|
|
68
78
|
maxComponentSkips: Number(process.env.EXPRESSIVECSS_MCP_MAX_COMPONENT_SKIPS || 7),
|
|
69
79
|
qaMaxFiles: Number(process.env.EXPRESSIVECSS_MCP_QA_MAX_FILES || DEFAULT_QA_MAX_FILES),
|
|
@@ -94,7 +104,7 @@ const LegacyPatternList = [
|
|
|
94
104
|
id: 'legacy-card-content',
|
|
95
105
|
severity: 'high',
|
|
96
106
|
description: '`.card-content` was removed; use the component’s documented child structure.',
|
|
97
|
-
pattern:
|
|
107
|
+
pattern: /(?<![-\w])card-content(?![-\w])/g,
|
|
98
108
|
},
|
|
99
109
|
{
|
|
100
110
|
id: 'legacy-nav-wrapper',
|
|
@@ -170,7 +180,11 @@ const TOOL_DESCRIPTIONS = {
|
|
|
170
180
|
},
|
|
171
181
|
component_syntax_expert: {
|
|
172
182
|
stage: 'Component Syntax Expert',
|
|
173
|
-
description: 'Return component syntax
|
|
183
|
+
description: 'Return complete component rules with detailed syntax by default. Select compact detail to omit prose/examples, includeCapabilities to override component capability detail, sections for documented options/methods, or foundations for typography, shape, or motion. Intentional omissions are disclosed.',
|
|
184
|
+
},
|
|
185
|
+
component_catalog: {
|
|
186
|
+
stage: 'Component Catalogue',
|
|
187
|
+
description: 'List or search bundled components by name, alias or compact description. Search returns bounded, labelled matches; results identify the bundled snapshot and optionally check target-project compatibility.',
|
|
174
188
|
},
|
|
175
189
|
quality_inspector: {
|
|
176
190
|
stage: 'Quality Inspector',
|
|
@@ -212,9 +226,19 @@ const pageArchitectSchema = {
|
|
|
212
226
|
|
|
213
227
|
const syntaxSchema = {
|
|
214
228
|
projectRoot: z.string().max(MAX_PROJECT_ROOT_CHARS).optional(),
|
|
229
|
+
detail: z.enum(['compact', 'detailed']).default('detailed'),
|
|
230
|
+
includeCapabilities: z.boolean().optional(),
|
|
215
231
|
components: z.array(z.string().max(MAX_COMPONENT_NAME_CHARS)).max(12).default([]),
|
|
216
232
|
foundations: z.array(z.enum(['typography', 'shape', 'motion'])).max(3).default([]),
|
|
233
|
+
sections: z.array(z.enum(['options', 'methods'])).max(2).refine((sections) => new Set(sections).size === sections.length, 'Request each section at most once').default([]),
|
|
234
|
+
workflowId: z.string().max(MAX_WORKFLOW_ID_CHARS).optional(),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const catalogSchema = {
|
|
238
|
+
projectRoot: z.string().max(MAX_PROJECT_ROOT_CHARS).optional(),
|
|
217
239
|
workflowId: z.string().max(MAX_WORKFLOW_ID_CHARS).optional(),
|
|
240
|
+
query: z.string().max(256).trim().min(1).optional(),
|
|
241
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
218
242
|
};
|
|
219
243
|
|
|
220
244
|
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
@@ -237,6 +261,40 @@ const qualityOutputSchema = stageOutputSchema.extend({
|
|
|
237
261
|
status: z.enum(['pass', 'warn', 'blocked', 'needs_fix']),
|
|
238
262
|
inspectionEvidence: inspectionEvidenceSchema.optional(), // Disabled tools return the shared blocked envelope.
|
|
239
263
|
});
|
|
264
|
+
const retryRequestSchema = z.discriminatedUnion('name', [
|
|
265
|
+
['setup_expert', setupSchema], ['creative_director', creativeSchema],
|
|
266
|
+
['page_architect', pageArchitectSchema], ['page_arcjitect', pageArchitectSchema],
|
|
267
|
+
['component_syntax_expert', syntaxSchema], ['component_catalog', catalogSchema],
|
|
268
|
+
].map(([name, schema]) => z.object({ name: z.literal(name), arguments: z.object(schema).strict() })));
|
|
269
|
+
const responseBudgetSchema = z.object({
|
|
270
|
+
maxBytes: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
271
|
+
delivery: z.enum(['complete', 'partial', 'error']),
|
|
272
|
+
omissions: z.array(z.object({
|
|
273
|
+
unit: z.enum(['field', 'component', 'foundation', 'entry', 'suggestion', 'architecture', 'request']),
|
|
274
|
+
index: z.number().int().nonnegative().optional(),
|
|
275
|
+
requested: z.string().optional(), slug: z.string().optional(),
|
|
276
|
+
field: z.enum(['contract', 'syntax', 'capability', 'options', 'methods']).optional(),
|
|
277
|
+
reason: z.literal('byte-budget'),
|
|
278
|
+
recovery: z.number().int().nonnegative(),
|
|
279
|
+
})),
|
|
280
|
+
recoveries: z.array(z.discriminatedUnion('action', [
|
|
281
|
+
z.object({ action: z.literal('retry'), request: retryRequestSchema }),
|
|
282
|
+
z.object({ action: z.literal('increase-budget'), configuration: z.literal(RESPONSE_BUDGET_SETTING) }),
|
|
283
|
+
])),
|
|
284
|
+
});
|
|
285
|
+
const guidanceOutputSchema = stageOutputSchema.extend({ responseBudget: responseBudgetSchema });
|
|
286
|
+
const syntaxOutputSchema = guidanceOutputSchema.extend({
|
|
287
|
+
// Disabled tools return only the shared blocked envelope.
|
|
288
|
+
detail: z.enum(['compact', 'detailed']).optional(),
|
|
289
|
+
includeCapabilities: z.boolean().optional(),
|
|
290
|
+
found: z.array(z.looseObject({
|
|
291
|
+
requestIndex: z.number().int().nonnegative(),
|
|
292
|
+
omittedFields: z.array(z.object({
|
|
293
|
+
field: z.enum(['contract', 'syntax', 'options', 'methods', 'capability']),
|
|
294
|
+
reason: z.enum(['compact-detail', 'not-requested', 'length-limit', 'byte-budget']),
|
|
295
|
+
})),
|
|
296
|
+
})).optional(),
|
|
297
|
+
});
|
|
240
298
|
const readAnnotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
241
299
|
|
|
242
300
|
const inspectSchema = {
|
|
@@ -288,13 +346,27 @@ function normalizeForMatch(value) {
|
|
|
288
346
|
|
|
289
347
|
function extractSection(text, heading) {
|
|
290
348
|
const marker = `#### ${heading}`;
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
349
|
+
const lines = text.split('\n');
|
|
350
|
+
let start = -1;
|
|
351
|
+
let fence = null;
|
|
352
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
353
|
+
const line = lines[index];
|
|
354
|
+
const delimiter = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/u);
|
|
355
|
+
if (fence) {
|
|
356
|
+
if (delimiter && delimiter[1][0] === fence[0] && delimiter[1].length >= fence.length && !delimiter[2].trim()) fence = null;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (delimiter) {
|
|
360
|
+
fence = delimiter[1];
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (start === -1) {
|
|
364
|
+
if (line.trim() === marker) start = index + 1;
|
|
365
|
+
} else if (/^ {0,3}#{1,4}(?:\s|$)/u.test(line)) {
|
|
366
|
+
return lines.slice(start, index).join('\n').trim();
|
|
367
|
+
}
|
|
294
368
|
}
|
|
295
|
-
|
|
296
|
-
const next = body.indexOf('\n#### ');
|
|
297
|
-
return (next === -1 ? body : body.slice(0, next)).trim();
|
|
369
|
+
return start === -1 ? '' : lines.slice(start).join('\n').trim();
|
|
298
370
|
}
|
|
299
371
|
|
|
300
372
|
function extractTitle(text) {
|
|
@@ -331,16 +403,116 @@ function clampText(input, maxChars) {
|
|
|
331
403
|
return `${text.slice(0, Math.max(0, maxChars - 40))}…(truncated)`;
|
|
332
404
|
}
|
|
333
405
|
|
|
334
|
-
function
|
|
406
|
+
function serializeToolResult(payload) {
|
|
335
407
|
return {
|
|
336
408
|
content: [{
|
|
337
409
|
type: 'text',
|
|
338
|
-
text: JSON.stringify(payload, null, 2),
|
|
410
|
+
text: JSON.stringify(payload, null, GUIDANCE_TOOLS.has(payload.stage) ? undefined : 2),
|
|
339
411
|
}],
|
|
340
412
|
structuredContent: payload,
|
|
413
|
+
...(payload.responseBudget?.delivery === 'error' ? { isError: true } : {}),
|
|
341
414
|
};
|
|
342
415
|
}
|
|
343
416
|
|
|
417
|
+
function toToolResult(payload, args = {}, guidanceContext) {
|
|
418
|
+
const result = () => serializeToolResult(payload);
|
|
419
|
+
if (!GUIDANCE_TOOLS.has(payload.stage)) return result();
|
|
420
|
+
payload.responseBudget = { maxBytes: SETTINGS.maxResponseBytes, delivery: 'complete', omissions: [], recoveries: [] };
|
|
421
|
+
const fits = () => Buffer.byteLength(JSON.stringify(result()), 'utf8') <= SETTINGS.maxResponseBytes;
|
|
422
|
+
if (fits()) return result();
|
|
423
|
+
|
|
424
|
+
const original = structuredClone(payload);
|
|
425
|
+
const context = { workflowId: payload.workflowId, ...(args.projectRoot === undefined ? {} : { projectRoot: args.projectRoot }) };
|
|
426
|
+
const increase = { action: 'increase-budget', configuration: RESPONSE_BUDGET_SETTING };
|
|
427
|
+
const omit = (identity, recovery) => {
|
|
428
|
+
const budget = payload.responseBudget;
|
|
429
|
+
// Share recovery records, especially operator-increase advice, to bound their overhead.
|
|
430
|
+
const key = JSON.stringify(recovery);
|
|
431
|
+
let index = budget.recoveries.findIndex((entry) => JSON.stringify(entry) === key);
|
|
432
|
+
if (index < 0) index = budget.recoveries.push(recovery) - 1;
|
|
433
|
+
budget.omissions.push({ ...identity, reason: 'byte-budget', recovery: index });
|
|
434
|
+
budget.delivery = recovery.action === 'increase-budget' ? 'error' : budget.delivery === 'error' ? 'error' : 'partial';
|
|
435
|
+
payload.status = original.status === 'blocked' || budget.delivery === 'error' ? 'blocked' : 'partial';
|
|
436
|
+
payload.coverageStatus = budget.delivery === 'error' ? 'response-budget-error' : 'partial-response-budget';
|
|
437
|
+
};
|
|
438
|
+
const retry = (arguments_) => ({ action: 'retry', request: retryRequestSchema.parse({ name: payload.stage, arguments: {
|
|
439
|
+
// Catalogue retrieval has no workflow state. Preserve its explicit target without
|
|
440
|
+
// repeating an optional correlation ID for every individually recoverable entry.
|
|
441
|
+
...(payload.stage === 'component_catalog' ? args.projectRoot === undefined ? {} : { projectRoot: args.projectRoot } : context),
|
|
442
|
+
...arguments_,
|
|
443
|
+
} }) });
|
|
444
|
+
const recoveryResultFits = (candidate) => {
|
|
445
|
+
candidate.responseBudget = { maxBytes: SETTINGS.maxResponseBytes, delivery: 'complete', omissions: [], recoveries: [] };
|
|
446
|
+
return Buffer.byteLength(JSON.stringify(serializeToolResult(candidate)), 'utf8') <= SETTINGS.maxResponseBytes;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
if (payload.stage === 'component_syntax_expert' && payload.found) {
|
|
450
|
+
const components = payload.found.map((entry, index) => ({ entry, index: entry.requestIndex ?? index }));
|
|
451
|
+
const syntaxRecovery = (entry, field) => {
|
|
452
|
+
const arguments_ = { components: [entry.slug], detail: field === 'contract' || field === 'syntax' ? 'detailed' : 'compact', includeCapabilities: field === 'capability', sections: ['options', 'methods'].includes(field) ? [field] : [] };
|
|
453
|
+
const recovery = retry(arguments_);
|
|
454
|
+
const candidate = buildSyntaxPayload(guidanceContext.catalog, guidanceContext.version, recovery.request.arguments);
|
|
455
|
+
return recoveryResultFits(candidate) ? recovery : increase;
|
|
456
|
+
};
|
|
457
|
+
for (const field of ['capability', 'contract', 'syntax', 'options', 'methods']) {
|
|
458
|
+
for (const { entry, index } of [...components].reverse()) {
|
|
459
|
+
if (!Object.hasOwn(entry, field)) continue;
|
|
460
|
+
const recovery = syntaxRecovery(original.found.find((row) => row.requestIndex === index) ?? entry, field);
|
|
461
|
+
delete entry[field];
|
|
462
|
+
entry.omittedFields = entry.omittedFields.filter((row) => row.field !== field);
|
|
463
|
+
entry.omittedFields.push({ field, reason: 'byte-budget' });
|
|
464
|
+
entry.omittedFields.sort((a, b) => ['contract', 'syntax', 'options', 'methods', 'capability'].indexOf(a.field) - ['contract', 'syntax', 'options', 'methods', 'capability'].indexOf(b.field));
|
|
465
|
+
omit({ unit: 'field', index, requested: args.components?.[index] ?? entry.slug, slug: entry.slug, field }, recovery);
|
|
466
|
+
if (fits()) return result();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
while (payload.found.length) {
|
|
470
|
+
const entry = payload.found.pop();
|
|
471
|
+
const index = entry.requestIndex;
|
|
472
|
+
omit({ unit: 'component', index, requested: args.components?.[index] ?? entry.slug, slug: entry.slug }, syntaxRecovery(original.found.find((row) => row.requestIndex === index), undefined));
|
|
473
|
+
payload.foundCount = payload.found.length;
|
|
474
|
+
if (fits()) return result();
|
|
475
|
+
}
|
|
476
|
+
while (payload.foundations.length) {
|
|
477
|
+
const index = payload.foundations.length - 1;
|
|
478
|
+
const entry = payload.foundations.pop();
|
|
479
|
+
let recovery = retry({ components: [], foundations: [args.foundations[index]], detail: 'compact', includeCapabilities: false });
|
|
480
|
+
const candidate = buildSyntaxPayload(guidanceContext.catalog, guidanceContext.version, recovery.request.arguments);
|
|
481
|
+
if (!recoveryResultFits(candidate)) recovery = increase;
|
|
482
|
+
omit({ unit: 'foundation', index, requested: args.foundations[index], slug: entry.slug }, recovery);
|
|
483
|
+
if (fits()) return result();
|
|
484
|
+
}
|
|
485
|
+
} else if (payload.stage === 'component_catalog' || payload.stage === 'creative_director') {
|
|
486
|
+
const field = payload.stage === 'component_catalog' ? 'entries' : 'suggestions';
|
|
487
|
+
while (payload[field].length) {
|
|
488
|
+
const index = payload[field].length - 1;
|
|
489
|
+
const entry = payload[field].pop();
|
|
490
|
+
const goal = entry.selectionSource === 'native-pattern' ? 'Choose persistent nonblocking inline feedback.' : `Choose ${entry.slug} for this interface.`;
|
|
491
|
+
let recovery = field === 'entries' ? retry({ query: entry.slug, limit: 1 }) : retry({ goal, maxSuggestions: 1 });
|
|
492
|
+
const candidate = (field === 'entries' ? buildCatalogPayload : buildCreativePayload)(guidanceContext.catalog, guidanceContext.version, recovery.request.arguments);
|
|
493
|
+
if (candidate[field][0]?.slug !== entry.slug || !recoveryResultFits(candidate)) recovery = increase;
|
|
494
|
+
omit({ unit: field === 'entries' ? 'entry' : 'suggestion', ...(field === 'entries' ? {} : { index }), slug: entry.slug }, recovery);
|
|
495
|
+
payload.count = payload[field].length;
|
|
496
|
+
if (field === 'suggestions') { payload.omittedCount += 1; payload.truncated = true; }
|
|
497
|
+
if (fits()) return result();
|
|
498
|
+
}
|
|
499
|
+
} else if (payload.architecture) {
|
|
500
|
+
payload.architecture = null;
|
|
501
|
+
omit({ unit: 'architecture' }, increase);
|
|
502
|
+
if (fits()) return result();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// No guidance is delivered by this error. Preserve actual checks and compatibility evidence.
|
|
506
|
+
const essential = ['workflowId', 'stage', 'checksPerformed', 'evidenceSources', 'uncheckedAreas', 'blockedChecks', 'contractCompatibility', 'contractProvenance', 'contractProvenanceDetails', 'contractVersion', 'sourceHash', 'guideSource', 'capabilityEvidence', 'skipped'];
|
|
507
|
+
payload = Object.fromEntries(essential.filter((key) => Object.hasOwn(original, key)).map((key) => [key, original[key]]));
|
|
508
|
+
payload.status = 'blocked';
|
|
509
|
+
payload.coverageStatus = 'response-budget-error';
|
|
510
|
+
payload.message = 'No requested guidance was delivered. Increase the operator response budget and repeat the request.';
|
|
511
|
+
payload.responseBudget = { maxBytes: SETTINGS.maxResponseBytes, delivery: 'error', omissions: [{ unit: 'request', reason: 'byte-budget', recovery: 0 }], recoveries: [increase] };
|
|
512
|
+
// The final transport check converts this into a protocol error if even evidence cannot fit.
|
|
513
|
+
return result();
|
|
514
|
+
}
|
|
515
|
+
|
|
344
516
|
function buildStagePayload(tool, result, workflowId) {
|
|
345
517
|
return {
|
|
346
518
|
workflowId: workflowId || randomUUID().slice(0, 12),
|
|
@@ -403,6 +575,8 @@ function parseGuide(file, content) {
|
|
|
403
575
|
const contract = extractSection(content, 'Contract');
|
|
404
576
|
const syntax = extractSection(content, 'Syntax');
|
|
405
577
|
const rules = extractRules(extractSection(content, 'Rules'));
|
|
578
|
+
const options = extractSection(content, 'Options');
|
|
579
|
+
const methods = extractSection(content, 'Methods');
|
|
406
580
|
const syntaxCode = extractFirstCodeBlock(syntax);
|
|
407
581
|
const syntaxLangMatch = syntax.match(/```\s*([a-z0-9+.-]+)/i);
|
|
408
582
|
const syntaxLanguage = syntaxLangMatch ? syntaxLangMatch[1].toLowerCase() : 'html';
|
|
@@ -413,15 +587,17 @@ function parseGuide(file, content) {
|
|
|
413
587
|
file,
|
|
414
588
|
slug,
|
|
415
589
|
title,
|
|
416
|
-
contract
|
|
590
|
+
contract,
|
|
417
591
|
syntax: {
|
|
418
592
|
language: syntaxLanguage,
|
|
419
|
-
code:
|
|
593
|
+
code: syntaxCode,
|
|
420
594
|
},
|
|
421
595
|
rules: rules.length ? rules : [
|
|
422
596
|
'Follow the component contract in the full documentation before adding optional attributes.',
|
|
423
597
|
'Keep runtime-owned state in framework initialization, not in static markup values.',
|
|
424
598
|
],
|
|
599
|
+
options: options || null,
|
|
600
|
+
methods: methods || null,
|
|
425
601
|
sourceUrl: docsMatch ? docsMatch[1] : null,
|
|
426
602
|
astroSource: repoMatch ? `https://github.com/BaezFJ/ExpressiveCSS/blob/master/docs/src/pages/${repoMatch[1]}.astro` : null,
|
|
427
603
|
text: `${title}\n${contract}\n${rules.join('\n')}`.toLowerCase(),
|
|
@@ -597,13 +773,13 @@ async function loadGuideCatalog(projectRoot) {
|
|
|
597
773
|
|
|
598
774
|
let provenance;
|
|
599
775
|
|
|
600
|
-
const guideDir = await resolveGuideDirectory(projectRoot);
|
|
776
|
+
const guideDir = projectRoot === undefined ? null : await resolveGuideDirectory(projectRoot);
|
|
601
777
|
const dirStat = guideDir ? await stat(guideDir).catch(() => null) : null;
|
|
602
|
-
const projectResolution = await resolveExpressiveVersion({
|
|
778
|
+
const projectResolution = projectRoot === undefined ? null : await resolveExpressiveVersion({
|
|
603
779
|
projectRoot,
|
|
604
780
|
contractVersion: bundledGuideCache.frameworkVersion,
|
|
605
781
|
});
|
|
606
|
-
const isFrameworkSource = projectResolution
|
|
782
|
+
const isFrameworkSource = projectResolution?.resolutionSource === 'framework-source';
|
|
607
783
|
if (dirStat?.isDirectory()) {
|
|
608
784
|
const localContractPath = path.join(projectRoot, 'skills', 'expressivecss', 'references', 'contract.json');
|
|
609
785
|
const localContractRead = await readInspectionFile(
|
|
@@ -650,7 +826,7 @@ async function loadGuideCatalog(projectRoot) {
|
|
|
650
826
|
|
|
651
827
|
return {
|
|
652
828
|
generatedAt: new Date().toISOString(),
|
|
653
|
-
projectRoot: path.resolve(projectRoot),
|
|
829
|
+
projectRoot: projectRoot === undefined ? null : path.resolve(projectRoot),
|
|
654
830
|
frameworkVersion: bundledGuideCache.frameworkVersion,
|
|
655
831
|
sourceHash: bundledGuideCache.sourceHash,
|
|
656
832
|
provenance,
|
|
@@ -884,6 +1060,8 @@ function inspectSemanticRules(snippet, maxIssues = MAX_STATIC_ISSUES, deadline =
|
|
|
884
1060
|
if (markupStructureExceedsLimit(snippet)) return markInspectionTruncated([], 'markup structure limit reached');
|
|
885
1061
|
const prefix = '<!doctype html><body>';
|
|
886
1062
|
const includeNodeLocations = snippet.length * (snippet.split('<').length - 1) <= MAX_LOCATED_MARKUP_WORK;
|
|
1063
|
+
// Loaded on first use: jsdom takes ~0.3 s to load, and most server starts never inspect markup.
|
|
1064
|
+
const { JSDOM } = createRequire(import.meta.url)('jsdom');
|
|
887
1065
|
const dom = new JSDOM(`${prefix}${snippet}</body>`, { includeNodeLocations });
|
|
888
1066
|
const { document } = dom.window;
|
|
889
1067
|
// Elements the parser creates without a start tag have no source offset, so they report the start.
|
|
@@ -1278,19 +1456,29 @@ function buildPageArchitecture(catalog, pageGoal, components = [], viewportTarge
|
|
|
1278
1456
|
};
|
|
1279
1457
|
}
|
|
1280
1458
|
|
|
1281
|
-
function summarizeGuide(guide) {
|
|
1459
|
+
function summarizeGuide(guide, { sections, detail, includeCapabilities }) {
|
|
1282
1460
|
return {
|
|
1283
1461
|
file: guide.file,
|
|
1284
1462
|
slug: guide.slug,
|
|
1285
1463
|
title: guide.title,
|
|
1286
1464
|
source: guide.sourceUrl,
|
|
1287
1465
|
docs: guide.astroSource,
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1466
|
+
...(detail === 'detailed' ? {
|
|
1467
|
+
contract: clampText(guide.contract, 900),
|
|
1468
|
+
syntax: {
|
|
1469
|
+
language: guide.syntax.language,
|
|
1470
|
+
example: guide.syntax.code,
|
|
1471
|
+
},
|
|
1472
|
+
} : {}),
|
|
1473
|
+
rules: guide.rules,
|
|
1474
|
+
...(sections.includes('options') ? { options: { status: guide.options ? 'documented' : 'absent', markdown: guide.options } } : {}),
|
|
1475
|
+
...(sections.includes('methods') ? { methods: { status: guide.methods ? 'documented' : 'absent', markdown: guide.methods } } : {}),
|
|
1476
|
+
omittedFields: [
|
|
1477
|
+
...(detail === 'compact' ? [{ field: 'contract', reason: 'compact-detail' }, { field: 'syntax', reason: 'compact-detail' }] : []),
|
|
1478
|
+
...['options', 'methods'].filter((field) => !sections.includes(field)).map((field) => ({ field, reason: 'not-requested' })),
|
|
1479
|
+
...(!includeCapabilities ? [{ field: 'capability', reason: 'not-requested' }] : []),
|
|
1480
|
+
...(detail === 'detailed' && guide.contract.length > 900 ? [{ field: 'contract', reason: 'length-limit' }] : []),
|
|
1481
|
+
].sort((a, b) => ['contract', 'syntax', 'options', 'methods', 'capability'].indexOf(a.field) - ['contract', 'syntax', 'options', 'methods', 'capability'].indexOf(b.field)),
|
|
1294
1482
|
};
|
|
1295
1483
|
}
|
|
1296
1484
|
|
|
@@ -1695,6 +1883,7 @@ const rulesSchemaParsed = z.object(rulesSchema);
|
|
|
1695
1883
|
const creativeSchemaParsed = z.object(creativeSchema);
|
|
1696
1884
|
const architectSchemaParsed = z.object(pageArchitectSchema);
|
|
1697
1885
|
const syntaxSchemaParsed = z.object(syntaxSchema).refine((value) => value.components.length + value.foundations.length > 0, 'Request at least one component or foundation');
|
|
1886
|
+
const catalogSchemaParsed = z.object(catalogSchema);
|
|
1698
1887
|
const qualitySchemaParsed = z.object(inspectSchema);
|
|
1699
1888
|
|
|
1700
1889
|
async function setupExpertHandler(args) {
|
|
@@ -1814,7 +2003,7 @@ async function setupExpertHandler(args) {
|
|
|
1814
2003
|
workflowId,
|
|
1815
2004
|
);
|
|
1816
2005
|
|
|
1817
|
-
return toToolResult(payload);
|
|
2006
|
+
return toToolResult(payload, { ...parsed, projectRoot });
|
|
1818
2007
|
}
|
|
1819
2008
|
|
|
1820
2009
|
async function rulesEnforcerHandler(args) {
|
|
@@ -1946,10 +2135,14 @@ async function creativeDirectorHandler(args) {
|
|
|
1946
2135
|
}
|
|
1947
2136
|
|
|
1948
2137
|
const parsed = creativeSchemaParsed.parse(args);
|
|
1949
|
-
const workflowId = parsed.workflowId || randomUUID();
|
|
1950
2138
|
const projectRoot = resolveProjectRoot(parsed.projectRoot);
|
|
1951
2139
|
const catalog = await loadGuideCatalog(projectRoot);
|
|
1952
2140
|
const version = await resolveAgainstContract(projectRoot, catalog.frameworkVersion);
|
|
2141
|
+
return toToolResult(buildCreativePayload(catalog, version, parsed), { ...parsed, projectRoot }, { catalog, version });
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function buildCreativePayload(catalog, version, parsed) {
|
|
2145
|
+
const workflowId = parsed.workflowId || randomUUID();
|
|
1953
2146
|
const provenanceBlock = provenanceBlockReason(catalog.provenance.status);
|
|
1954
2147
|
const contractSafe = version.status === 'match' && !provenanceBlock;
|
|
1955
2148
|
|
|
@@ -1959,7 +2152,7 @@ async function creativeDirectorHandler(args) {
|
|
|
1959
2152
|
: { suggestions: [], truncated: false, omittedCount: 0 };
|
|
1960
2153
|
const { suggestions, truncated, omittedCount } = candidateResult;
|
|
1961
2154
|
|
|
1962
|
-
|
|
2155
|
+
return buildStagePayload(
|
|
1963
2156
|
'creative_director',
|
|
1964
2157
|
{
|
|
1965
2158
|
goal: parsed.goal,
|
|
@@ -1997,8 +2190,6 @@ async function creativeDirectorHandler(args) {
|
|
|
1997
2190
|
},
|
|
1998
2191
|
workflowId,
|
|
1999
2192
|
);
|
|
2000
|
-
|
|
2001
|
-
return toToolResult(payload);
|
|
2002
2193
|
}
|
|
2003
2194
|
|
|
2004
2195
|
async function pageArchitectHandler(args, stage = 'page_architect') {
|
|
@@ -2072,7 +2263,7 @@ async function pageArchitectHandler(args, stage = 'page_architect') {
|
|
|
2072
2263
|
workflowId,
|
|
2073
2264
|
);
|
|
2074
2265
|
|
|
2075
|
-
return toToolResult(payload);
|
|
2266
|
+
return toToolResult(payload, { ...parsed, projectRoot });
|
|
2076
2267
|
}
|
|
2077
2268
|
|
|
2078
2269
|
async function componentSyntaxExpertHandler(args) {
|
|
@@ -2081,10 +2272,15 @@ async function componentSyntaxExpertHandler(args) {
|
|
|
2081
2272
|
}
|
|
2082
2273
|
|
|
2083
2274
|
const parsed = syntaxSchemaParsed.parse(args);
|
|
2084
|
-
const workflowId = parsed.workflowId || randomUUID();
|
|
2085
2275
|
const projectRoot = resolveProjectRoot(parsed.projectRoot);
|
|
2086
2276
|
const catalog = await loadGuideCatalog(projectRoot);
|
|
2087
2277
|
const version = await resolveAgainstContract(projectRoot, catalog.frameworkVersion);
|
|
2278
|
+
return toToolResult(buildSyntaxPayload(catalog, version, parsed), { ...parsed, projectRoot }, { catalog, version });
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
function buildSyntaxPayload(catalog, version, parsed) {
|
|
2282
|
+
const includeCapabilities = parsed.includeCapabilities ?? parsed.detail === 'detailed';
|
|
2283
|
+
const workflowId = parsed.workflowId || randomUUID();
|
|
2088
2284
|
const provenanceBlock = provenanceBlockReason(catalog.provenance.status);
|
|
2089
2285
|
|
|
2090
2286
|
const capabilitySafe = version.status === 'match' && !provenanceBlock && CAPABILITY_ROADMAP.frameworkVersion === catalog.frameworkVersion;
|
|
@@ -2092,7 +2288,7 @@ async function componentSyntaxExpertHandler(args) {
|
|
|
2092
2288
|
const found = [];
|
|
2093
2289
|
const missing = [];
|
|
2094
2290
|
|
|
2095
|
-
for (const component of requested) {
|
|
2291
|
+
for (const [requestIndex, component] of requested.entries()) {
|
|
2096
2292
|
const guide = findGuideByName(catalog, component, false);
|
|
2097
2293
|
if (!guide) {
|
|
2098
2294
|
const skipLimit = Math.min(Math.max(SETTINGS.maxComponentSkips, 1), 20);
|
|
@@ -2101,13 +2297,19 @@ async function componentSyntaxExpertHandler(args) {
|
|
|
2101
2297
|
nearest: nearestMatches(catalog, component, skipLimit).map((row) => row.slug),
|
|
2102
2298
|
});
|
|
2103
2299
|
} else {
|
|
2104
|
-
found.push({
|
|
2300
|
+
found.push({
|
|
2301
|
+
requestIndex,
|
|
2302
|
+
...summarizeGuide(guide, { ...parsed, includeCapabilities }),
|
|
2303
|
+
...(includeCapabilities ? { capability: capabilitySafe ? CAPABILITIES_BY_SLUG.get(guide.slug) ?? null : null } : {}),
|
|
2304
|
+
});
|
|
2105
2305
|
}
|
|
2106
2306
|
}
|
|
2107
2307
|
|
|
2108
|
-
|
|
2308
|
+
return buildStagePayload(
|
|
2109
2309
|
'component_syntax_expert',
|
|
2110
2310
|
{
|
|
2311
|
+
detail: parsed.detail,
|
|
2312
|
+
includeCapabilities,
|
|
2111
2313
|
foundCount: found.length,
|
|
2112
2314
|
foundations: capabilitySafe ? parsed.foundations.map((slug) => CAPABILITIES_BY_SLUG.get(slug)) : [],
|
|
2113
2315
|
capabilityEvidence: { status: capabilitySafe ? 'bundled-review-snapshot' : 'blocked', basis: CAPABILITY_ROADMAP.basis, browserRun: capabilitySafe ? CAPABILITY_ROADMAP.browserRun : null },
|
|
@@ -2142,8 +2344,74 @@ async function componentSyntaxExpertHandler(args) {
|
|
|
2142
2344
|
},
|
|
2143
2345
|
workflowId,
|
|
2144
2346
|
);
|
|
2347
|
+
}
|
|
2145
2348
|
|
|
2146
|
-
|
|
2349
|
+
async function componentCatalogHandler(args) {
|
|
2350
|
+
const parsed = catalogSchemaParsed.parse(args);
|
|
2351
|
+
const projectRoot = parsed.projectRoot === undefined ? undefined : path.resolve(parsed.projectRoot);
|
|
2352
|
+
const catalog = await loadGuideCatalog(projectRoot);
|
|
2353
|
+
const version = projectRoot === undefined ? null : await resolveAgainstContract(projectRoot, catalog.frameworkVersion);
|
|
2354
|
+
return toToolResult(buildCatalogPayload(catalog, version, parsed), parsed, { catalog, version });
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function buildCatalogPayload(catalog, version, parsed) {
|
|
2358
|
+
const provenanceBlock = provenanceBlockReason(catalog.provenance.status);
|
|
2359
|
+
let entries = [...catalog.components.values()].map((guide) => {
|
|
2360
|
+
const decision = COMPONENT_DECISIONS_BY_SLUG.get(guide.slug);
|
|
2361
|
+
return {
|
|
2362
|
+
slug: guide.slug,
|
|
2363
|
+
title: guide.title,
|
|
2364
|
+
description: [...(decision?.useWhen ?? []), ...(decision?.jobs ?? [])].find((value) => typeof value === 'string' && value.trim()) ?? null,
|
|
2365
|
+
aliases: decision?.aliases ?? [],
|
|
2366
|
+
runtime: decision?.runtime ?? null,
|
|
2367
|
+
docs: guide.sourceUrl,
|
|
2368
|
+
};
|
|
2369
|
+
}).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
2370
|
+
|
|
2371
|
+
let search = {};
|
|
2372
|
+
if (parsed.query !== undefined) {
|
|
2373
|
+
const query = normalizeForMatch(parsed.query);
|
|
2374
|
+
const tokens = query.split('-').filter(Boolean);
|
|
2375
|
+
const matchOrder = ['exact-name', 'exact-alias', 'heuristic'];
|
|
2376
|
+
entries = entries.flatMap((entry) => {
|
|
2377
|
+
const names = [entry.slug, entry.title].map(normalizeForMatch);
|
|
2378
|
+
const aliases = entry.aliases.map(normalizeForMatch);
|
|
2379
|
+
const fields = [...names, ...aliases, normalizeForMatch(entry.description)];
|
|
2380
|
+
const matchType = query && names.includes(query) ? 'exact-name'
|
|
2381
|
+
: query && aliases.includes(query) ? 'exact-alias'
|
|
2382
|
+
: tokens.length && tokens.every((token) => fields.some((field) => field.includes(token))) ? 'heuristic' : null;
|
|
2383
|
+
return matchType ? [{ ...entry, matchType }] : [];
|
|
2384
|
+
}).sort((a, b) => matchOrder.indexOf(a.matchType) - matchOrder.indexOf(b.matchType) || a.slug.localeCompare(b.slug));
|
|
2385
|
+
const totalMatches = entries.length;
|
|
2386
|
+
const limit = parsed.limit ?? 10;
|
|
2387
|
+
entries = entries.slice(0, limit);
|
|
2388
|
+
const omittedCount = totalMatches - entries.length;
|
|
2389
|
+
search = { query: parsed.query, limit, totalMatches, omittedCount, truncated: omittedCount > 0 };
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
return buildStagePayload('component_catalog', {
|
|
2393
|
+
entries,
|
|
2394
|
+
count: entries.length,
|
|
2395
|
+
...search,
|
|
2396
|
+
contractVersion: catalog.frameworkVersion,
|
|
2397
|
+
sourceHash: catalog.sourceHash,
|
|
2398
|
+
guideSource: catalog.guideSource,
|
|
2399
|
+
status: (!version || version.status === 'match') && !provenanceBlock ? 'available' : 'blocked',
|
|
2400
|
+
checksPerformed: [parsed.query === undefined ? 'bundled component catalogue listing' : 'bundled component catalogue search', ...(version ? ['target contract resolution', 'contract provenance validation'] : [])],
|
|
2401
|
+
evidenceSources: ['bundled:component-guides.json', 'bundled:component-decisions.json', 'bundled:contract.json', ...(version ? [version.resolutionSource] : [])],
|
|
2402
|
+
uncheckedAreas: [
|
|
2403
|
+
...(!version ? ['target-project compatibility', 'target-project provenance'] : []),
|
|
2404
|
+
'rendered component behavior', 'visual hierarchy', 'responsive composition', 'keyboard and assistive-technology behavior',
|
|
2405
|
+
],
|
|
2406
|
+
contractCompatibility: version?.status ?? 'unknown',
|
|
2407
|
+
contractProvenance: catalog.provenance.status,
|
|
2408
|
+
contractProvenanceDetails: catalog.provenance,
|
|
2409
|
+
coverageStatus: parsed.query === undefined ? 'complete-bundled-catalogue' : search.truncated ? 'partial-search-results' : 'complete-search-results',
|
|
2410
|
+
blockedChecks: [
|
|
2411
|
+
...(version && version.status !== 'match' ? ['target-version contract checks'] : []),
|
|
2412
|
+
...(provenanceBlock ? [provenanceBlock] : []),
|
|
2413
|
+
],
|
|
2414
|
+
}, parsed.workflowId);
|
|
2147
2415
|
}
|
|
2148
2416
|
|
|
2149
2417
|
async function qualityInspectorHandler(args) {
|
|
@@ -2315,6 +2583,105 @@ async function qualityInspectorHandler(args) {
|
|
|
2315
2583
|
return toToolResult(payload);
|
|
2316
2584
|
}
|
|
2317
2585
|
|
|
2586
|
+
const COMPONENT_SECTIONS = ['contract', 'syntax', 'rules', 'options', 'methods'];
|
|
2587
|
+
|
|
2588
|
+
function resourceRequiredBytes(result) {
|
|
2589
|
+
// A retry's budget field can need more decimal digits. Include those bytes too.
|
|
2590
|
+
const retry = structuredClone(result);
|
|
2591
|
+
const payload = JSON.parse(retry.contents[0].text);
|
|
2592
|
+
let requiredBytes = Buffer.byteLength(JSON.stringify(retry), 'utf8');
|
|
2593
|
+
for (;;) {
|
|
2594
|
+
payload.responseBudget.maxBytes = requiredBytes;
|
|
2595
|
+
retry.contents[0].text = JSON.stringify(payload);
|
|
2596
|
+
const measured = Buffer.byteLength(JSON.stringify(retry), 'utf8');
|
|
2597
|
+
if (measured <= requiredBytes) return requiredBytes;
|
|
2598
|
+
requiredBytes = measured;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
function resourceBudgetError(uri, result, resources) {
|
|
2603
|
+
const requiredBytes = resourceRequiredBytes(result);
|
|
2604
|
+
const data = { uri, maxBytes: SETTINGS.maxResponseBytes, requiredBytes, setting: RESPONSE_BUDGET_SETTING };
|
|
2605
|
+
if (!resources.sections.has(uri)) {
|
|
2606
|
+
return {
|
|
2607
|
+
code: -32001,
|
|
2608
|
+
message: `Response budget cannot fit the complete catalogue resource. Increase ${RESPONSE_BUDGET_SETTING} to at least ${requiredBytes} and restart.`,
|
|
2609
|
+
data,
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
// Recover a whole guide through the section URIs whose complete reads fit this budget.
|
|
2613
|
+
const fits = (sectionUri) => Buffer.byteLength(JSON.stringify(resources.readers.get(sectionUri)()), 'utf8') <= SETTINGS.maxResponseBytes;
|
|
2614
|
+
const sections = resources.sections.get(uri);
|
|
2615
|
+
const recoveries = sections.length > 1 ? sections.filter(fits) : [];
|
|
2616
|
+
return {
|
|
2617
|
+
code: -32001,
|
|
2618
|
+
message: `Response budget cannot fit the complete component resource. Read the listed section URIs or increase ${RESPONSE_BUDGET_SETTING} to at least ${requiredBytes} and restart.`,
|
|
2619
|
+
data: {
|
|
2620
|
+
...data,
|
|
2621
|
+
recoveries,
|
|
2622
|
+
unrecoverableSections: sections.filter((sectionUri) => !recoveries.includes(sectionUri)).map((sectionUri) => sectionUri.slice(sectionUri.lastIndexOf('/') + 1)),
|
|
2623
|
+
},
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
function bundledResources(catalog, catalogUri, catalogSnapshot) {
|
|
2628
|
+
const base = catalogUri.replace('expressivecss://catalogue/', 'expressivecss://components/');
|
|
2629
|
+
const jsonResource = (uri, payload) => () => ({
|
|
2630
|
+
contents: [{
|
|
2631
|
+
uri,
|
|
2632
|
+
mimeType: 'application/json',
|
|
2633
|
+
text: JSON.stringify({
|
|
2634
|
+
...payload,
|
|
2635
|
+
responseBudget: { maxBytes: SETTINGS.maxResponseBytes, delivery: 'complete', omissions: [], recoveries: [] },
|
|
2636
|
+
}),
|
|
2637
|
+
}],
|
|
2638
|
+
});
|
|
2639
|
+
const readers = new Map([[catalogUri, jsonResource(catalogUri, { schemaVersion: 1, ...catalogSnapshot })]]);
|
|
2640
|
+
// Whole-guide and section URIs map to the section URIs that can recover them.
|
|
2641
|
+
const sections = new Map();
|
|
2642
|
+
const guides = [];
|
|
2643
|
+
const provenanceBlock = provenanceBlockReason(catalog.provenance.status);
|
|
2644
|
+
for (const guide of [...catalog.components.values()].sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
2645
|
+
const uri = `${base}/${guide.slug}`;
|
|
2646
|
+
const identity = {
|
|
2647
|
+
schemaVersion: 1,
|
|
2648
|
+
slug: guide.slug,
|
|
2649
|
+
title: guide.title,
|
|
2650
|
+
docs: guide.sourceUrl,
|
|
2651
|
+
repositorySource: guide.astroSource,
|
|
2652
|
+
contractVersion: catalog.frameworkVersion,
|
|
2653
|
+
sourceHash: catalog.sourceHash,
|
|
2654
|
+
guideSource: catalog.guideSource,
|
|
2655
|
+
};
|
|
2656
|
+
const fields = {
|
|
2657
|
+
contract: guide.contract,
|
|
2658
|
+
syntax: { language: guide.syntax.language, example: guide.syntax.code },
|
|
2659
|
+
rules: guide.rules,
|
|
2660
|
+
options: { status: guide.options ? 'documented' : 'absent', markdown: guide.options },
|
|
2661
|
+
methods: { status: guide.methods ? 'documented' : 'absent', markdown: guide.methods },
|
|
2662
|
+
};
|
|
2663
|
+
const evidence = (coverageStatus) => ({
|
|
2664
|
+
status: provenanceBlock ? 'blocked' : 'available',
|
|
2665
|
+
checksPerformed: ['bundled component guide lookup'],
|
|
2666
|
+
evidenceSources: [`${catalog.guideSource}:${guide.file}`, 'bundled:contract.json'],
|
|
2667
|
+
uncheckedAreas: catalogSnapshot.uncheckedAreas,
|
|
2668
|
+
contractCompatibility: 'unknown',
|
|
2669
|
+
contractProvenance: catalog.provenance.status,
|
|
2670
|
+
contractProvenanceDetails: catalog.provenance,
|
|
2671
|
+
coverageStatus,
|
|
2672
|
+
blockedChecks: provenanceBlock ? [provenanceBlock] : [],
|
|
2673
|
+
});
|
|
2674
|
+
readers.set(uri, jsonResource(uri, { ...identity, ...fields, ...evidence('complete-bundled-guide') }));
|
|
2675
|
+
guides.push(guide);
|
|
2676
|
+
sections.set(uri, COMPONENT_SECTIONS.map((name) => `${uri}/${name}`));
|
|
2677
|
+
for (const name of COMPONENT_SECTIONS) {
|
|
2678
|
+
readers.set(`${uri}/${name}`, jsonResource(`${uri}/${name}`, { ...identity, section: name, [name]: fields[name], ...evidence('complete-bundled-guide-section') }));
|
|
2679
|
+
sections.set(`${uri}/${name}`, [`${uri}/${name}`]);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return { base, readers, sections, guides };
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2318
2685
|
async function startServer() {
|
|
2319
2686
|
const server = new McpServer(
|
|
2320
2687
|
{
|
|
@@ -2329,9 +2696,9 @@ async function startServer() {
|
|
|
2329
2696
|
);
|
|
2330
2697
|
|
|
2331
2698
|
server.registerTool('setup_expert', {
|
|
2332
|
-
description: TOOL_DESCRIPTIONS.setup_expert.description,
|
|
2699
|
+
description: TOOL_DESCRIPTIONS.setup_expert.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2333
2700
|
inputSchema: setupSchema,
|
|
2334
|
-
outputSchema:
|
|
2701
|
+
outputSchema: guidanceOutputSchema,
|
|
2335
2702
|
annotations: readAnnotations,
|
|
2336
2703
|
}, setupExpertHandler);
|
|
2337
2704
|
|
|
@@ -2343,33 +2710,40 @@ async function startServer() {
|
|
|
2343
2710
|
}, rulesEnforcerHandler);
|
|
2344
2711
|
|
|
2345
2712
|
server.registerTool('creative_director', {
|
|
2346
|
-
description: TOOL_DESCRIPTIONS.creative_director.description,
|
|
2713
|
+
description: TOOL_DESCRIPTIONS.creative_director.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2347
2714
|
inputSchema: creativeSchema,
|
|
2348
|
-
outputSchema:
|
|
2715
|
+
outputSchema: guidanceOutputSchema,
|
|
2349
2716
|
annotations: readAnnotations,
|
|
2350
2717
|
}, creativeDirectorHandler);
|
|
2351
2718
|
|
|
2352
2719
|
server.registerTool('page_architect', {
|
|
2353
|
-
description: TOOL_DESCRIPTIONS.page_architect.description,
|
|
2720
|
+
description: TOOL_DESCRIPTIONS.page_architect.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2354
2721
|
inputSchema: pageArchitectSchema,
|
|
2355
|
-
outputSchema:
|
|
2722
|
+
outputSchema: guidanceOutputSchema,
|
|
2356
2723
|
annotations: readAnnotations,
|
|
2357
2724
|
}, (args) => pageArchitectHandler(args, 'page_architect'));
|
|
2358
2725
|
|
|
2359
2726
|
server.registerTool('page_arcjitect', {
|
|
2360
|
-
description: TOOL_DESCRIPTIONS.page_arcjitect.description,
|
|
2727
|
+
description: TOOL_DESCRIPTIONS.page_arcjitect.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2361
2728
|
inputSchema: pageArchitectSchema,
|
|
2362
|
-
outputSchema:
|
|
2729
|
+
outputSchema: guidanceOutputSchema,
|
|
2363
2730
|
annotations: readAnnotations,
|
|
2364
2731
|
}, (args) => pageArchitectHandler(args, 'page_arcjitect'));
|
|
2365
2732
|
|
|
2366
2733
|
server.registerTool('component_syntax_expert', {
|
|
2367
|
-
description: TOOL_DESCRIPTIONS.component_syntax_expert.description,
|
|
2734
|
+
description: TOOL_DESCRIPTIONS.component_syntax_expert.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2368
2735
|
inputSchema: syntaxSchema,
|
|
2369
|
-
outputSchema:
|
|
2736
|
+
outputSchema: syntaxOutputSchema,
|
|
2370
2737
|
annotations: readAnnotations,
|
|
2371
2738
|
}, componentSyntaxExpertHandler);
|
|
2372
2739
|
|
|
2740
|
+
server.registerTool('component_catalog', {
|
|
2741
|
+
description: TOOL_DESCRIPTIONS.component_catalog.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2742
|
+
inputSchema: catalogSchema,
|
|
2743
|
+
outputSchema: guidanceOutputSchema,
|
|
2744
|
+
annotations: readAnnotations,
|
|
2745
|
+
}, componentCatalogHandler);
|
|
2746
|
+
|
|
2373
2747
|
server.registerTool('quality_inspector', {
|
|
2374
2748
|
description: TOOL_DESCRIPTIONS.quality_inspector.description,
|
|
2375
2749
|
inputSchema: inspectSchema,
|
|
@@ -2377,7 +2751,75 @@ async function startServer() {
|
|
|
2377
2751
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2378
2752
|
}, qualityInspectorHandler);
|
|
2379
2753
|
|
|
2754
|
+
const catalog = await loadGuideCatalog(undefined);
|
|
2755
|
+
const catalogUri = `expressivecss://catalogue/${encodeURIComponent(catalog.frameworkVersion)}/${catalog.sourceHash}`;
|
|
2756
|
+
const { stage, workflowId, ...snapshot } = buildCatalogPayload(catalog, null, { workflowId: 'catalogue-resource' });
|
|
2757
|
+
const resources = bundledResources(catalog, catalogUri, snapshot);
|
|
2758
|
+
const readResource = async (uri) => {
|
|
2759
|
+
const reader = resources.readers.get(uri);
|
|
2760
|
+
if (!reader) throw new McpError(-32002, 'Resource not found. Use resources/list or resources/templates/list to discover the current bundled snapshot.');
|
|
2761
|
+
return reader();
|
|
2762
|
+
};
|
|
2763
|
+
server.registerResource('component_catalog', catalogUri, {
|
|
2764
|
+
title: 'ExpressiveCSS component catalogue',
|
|
2765
|
+
description: 'Compact component metadata for the current bundled snapshot only. Consumer compatibility remains unchecked.',
|
|
2766
|
+
mimeType: 'application/json',
|
|
2767
|
+
}, () => readResource(catalogUri));
|
|
2768
|
+
// Case-insensitive prefix completion over canonical names only; the SDK caps values at 100.
|
|
2769
|
+
const startingWith = (names) => (value) => names.filter((name) => name.startsWith(value.toLowerCase()));
|
|
2770
|
+
const slug = startingWith(resources.guides.map((guide) => guide.slug));
|
|
2771
|
+
server.registerResource('component_guide', new ResourceTemplate(`${resources.base}/{slug}`, {
|
|
2772
|
+
list: async () => ({
|
|
2773
|
+
resources: resources.guides.map((guide) => ({ uri: `${resources.base}/${guide.slug}`, name: guide.slug, title: `ExpressiveCSS ${guide.title} guide` })),
|
|
2774
|
+
}),
|
|
2775
|
+
complete: { slug },
|
|
2776
|
+
}), {
|
|
2777
|
+
title: 'ExpressiveCSS component guide',
|
|
2778
|
+
description: 'Complete contract, syntax, rules, Options and Methods for one component in the current bundled snapshot only. Use a canonical slug; consumer compatibility remains unchecked.',
|
|
2779
|
+
mimeType: 'application/json',
|
|
2780
|
+
}, (uri) => readResource(uri.href));
|
|
2781
|
+
server.registerResource('component_guide_section', new ResourceTemplate(`${resources.base}/{slug}/{section}`, {
|
|
2782
|
+
list: undefined,
|
|
2783
|
+
complete: { slug, section: startingWith(COMPONENT_SECTIONS) },
|
|
2784
|
+
}), {
|
|
2785
|
+
title: 'ExpressiveCSS component guide section',
|
|
2786
|
+
description: `One complete section of a component guide in the current bundled snapshot only. Sections: ${COMPONENT_SECTIONS.join(', ')}. Use a canonical slug; consumer compatibility remains unchecked.`,
|
|
2787
|
+
mimeType: 'application/json',
|
|
2788
|
+
}, (uri) => readResource(uri.href));
|
|
2789
|
+
// SDK v1 normalizes URLs and uses -32602 for missing resources. Require an
|
|
2790
|
+
// advertised identity verbatim and return the resource-not-found protocol code.
|
|
2791
|
+
server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => readResource(request.params.uri));
|
|
2792
|
+
|
|
2380
2793
|
const transport = new StdioServerTransport();
|
|
2794
|
+
// Check the wire result after SDK error handling too. SDK exceptions otherwise become
|
|
2795
|
+
// unbounded isError results, including validation errors before our handlers run.
|
|
2796
|
+
const scopedRequests = new Map();
|
|
2797
|
+
const start = transport.start.bind(transport);
|
|
2798
|
+
const send = transport.send.bind(transport);
|
|
2799
|
+
transport.start = async () => {
|
|
2800
|
+
const receive = transport.onmessage;
|
|
2801
|
+
transport.onmessage = (message, extra) => {
|
|
2802
|
+
if (Object.hasOwn(message, 'id')) {
|
|
2803
|
+
if (message.method === 'tools/call' && GUIDANCE_TOOLS.has(message.params?.name)) scopedRequests.set(message.id, 'tool');
|
|
2804
|
+
if (message.method === 'resources/read') scopedRequests.set(message.id, { uri: message.params?.uri });
|
|
2805
|
+
}
|
|
2806
|
+
receive(message, extra);
|
|
2807
|
+
};
|
|
2808
|
+
await start();
|
|
2809
|
+
};
|
|
2810
|
+
transport.send = async (message, options) => {
|
|
2811
|
+
const kind = scopedRequests.get(message.id);
|
|
2812
|
+
scopedRequests.delete(message.id);
|
|
2813
|
+
if (kind && Object.hasOwn(message, 'result')
|
|
2814
|
+
&& Buffer.byteLength(JSON.stringify(message.result), 'utf8') > SETTINGS.maxResponseBytes) {
|
|
2815
|
+
const error = kind.uri ? resourceBudgetError(kind.uri, message.result, resources)
|
|
2816
|
+
: { code: -32001, message: `Response budget cannot fit a truthful tool result. Increase ${RESPONSE_BUDGET_SETTING}.` };
|
|
2817
|
+
message = { jsonrpc: '2.0', id: message.id, error };
|
|
2818
|
+
}
|
|
2819
|
+
await send(message, options);
|
|
2820
|
+
};
|
|
2821
|
+
const close = transport.close.bind(transport);
|
|
2822
|
+
transport.close = async () => { try { await close(); } finally { scopedRequests.clear(); } };
|
|
2381
2823
|
await server.connect(transport);
|
|
2382
2824
|
console.error('ExpressiveCSS MCP server running on stdio transport.');
|
|
2383
2825
|
}
|