@expressivecss/mcp-server 0.2.2 → 0.3.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/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +76 -0
- package/README.md +485 -3
- package/package.json +1 -1
- package/server.js +484 -44
- package/smoke.mjs +1221 -3
package/server.js
CHANGED
|
@@ -7,8 +7,9 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { JSDOM } from 'jsdom';
|
|
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),
|
|
@@ -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,
|
|
@@ -1278,19 +1454,29 @@ function buildPageArchitecture(catalog, pageGoal, components = [], viewportTarge
|
|
|
1278
1454
|
};
|
|
1279
1455
|
}
|
|
1280
1456
|
|
|
1281
|
-
function summarizeGuide(guide) {
|
|
1457
|
+
function summarizeGuide(guide, { sections, detail, includeCapabilities }) {
|
|
1282
1458
|
return {
|
|
1283
1459
|
file: guide.file,
|
|
1284
1460
|
slug: guide.slug,
|
|
1285
1461
|
title: guide.title,
|
|
1286
1462
|
source: guide.sourceUrl,
|
|
1287
1463
|
docs: guide.astroSource,
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1464
|
+
...(detail === 'detailed' ? {
|
|
1465
|
+
contract: clampText(guide.contract, 900),
|
|
1466
|
+
syntax: {
|
|
1467
|
+
language: guide.syntax.language,
|
|
1468
|
+
example: guide.syntax.code,
|
|
1469
|
+
},
|
|
1470
|
+
} : {}),
|
|
1471
|
+
rules: guide.rules,
|
|
1472
|
+
...(sections.includes('options') ? { options: { status: guide.options ? 'documented' : 'absent', markdown: guide.options } } : {}),
|
|
1473
|
+
...(sections.includes('methods') ? { methods: { status: guide.methods ? 'documented' : 'absent', markdown: guide.methods } } : {}),
|
|
1474
|
+
omittedFields: [
|
|
1475
|
+
...(detail === 'compact' ? [{ field: 'contract', reason: 'compact-detail' }, { field: 'syntax', reason: 'compact-detail' }] : []),
|
|
1476
|
+
...['options', 'methods'].filter((field) => !sections.includes(field)).map((field) => ({ field, reason: 'not-requested' })),
|
|
1477
|
+
...(!includeCapabilities ? [{ field: 'capability', reason: 'not-requested' }] : []),
|
|
1478
|
+
...(detail === 'detailed' && guide.contract.length > 900 ? [{ field: 'contract', reason: 'length-limit' }] : []),
|
|
1479
|
+
].sort((a, b) => ['contract', 'syntax', 'options', 'methods', 'capability'].indexOf(a.field) - ['contract', 'syntax', 'options', 'methods', 'capability'].indexOf(b.field)),
|
|
1294
1480
|
};
|
|
1295
1481
|
}
|
|
1296
1482
|
|
|
@@ -1695,6 +1881,7 @@ const rulesSchemaParsed = z.object(rulesSchema);
|
|
|
1695
1881
|
const creativeSchemaParsed = z.object(creativeSchema);
|
|
1696
1882
|
const architectSchemaParsed = z.object(pageArchitectSchema);
|
|
1697
1883
|
const syntaxSchemaParsed = z.object(syntaxSchema).refine((value) => value.components.length + value.foundations.length > 0, 'Request at least one component or foundation');
|
|
1884
|
+
const catalogSchemaParsed = z.object(catalogSchema);
|
|
1698
1885
|
const qualitySchemaParsed = z.object(inspectSchema);
|
|
1699
1886
|
|
|
1700
1887
|
async function setupExpertHandler(args) {
|
|
@@ -1814,7 +2001,7 @@ async function setupExpertHandler(args) {
|
|
|
1814
2001
|
workflowId,
|
|
1815
2002
|
);
|
|
1816
2003
|
|
|
1817
|
-
return toToolResult(payload);
|
|
2004
|
+
return toToolResult(payload, { ...parsed, projectRoot });
|
|
1818
2005
|
}
|
|
1819
2006
|
|
|
1820
2007
|
async function rulesEnforcerHandler(args) {
|
|
@@ -1946,10 +2133,14 @@ async function creativeDirectorHandler(args) {
|
|
|
1946
2133
|
}
|
|
1947
2134
|
|
|
1948
2135
|
const parsed = creativeSchemaParsed.parse(args);
|
|
1949
|
-
const workflowId = parsed.workflowId || randomUUID();
|
|
1950
2136
|
const projectRoot = resolveProjectRoot(parsed.projectRoot);
|
|
1951
2137
|
const catalog = await loadGuideCatalog(projectRoot);
|
|
1952
2138
|
const version = await resolveAgainstContract(projectRoot, catalog.frameworkVersion);
|
|
2139
|
+
return toToolResult(buildCreativePayload(catalog, version, parsed), { ...parsed, projectRoot }, { catalog, version });
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function buildCreativePayload(catalog, version, parsed) {
|
|
2143
|
+
const workflowId = parsed.workflowId || randomUUID();
|
|
1953
2144
|
const provenanceBlock = provenanceBlockReason(catalog.provenance.status);
|
|
1954
2145
|
const contractSafe = version.status === 'match' && !provenanceBlock;
|
|
1955
2146
|
|
|
@@ -1959,7 +2150,7 @@ async function creativeDirectorHandler(args) {
|
|
|
1959
2150
|
: { suggestions: [], truncated: false, omittedCount: 0 };
|
|
1960
2151
|
const { suggestions, truncated, omittedCount } = candidateResult;
|
|
1961
2152
|
|
|
1962
|
-
|
|
2153
|
+
return buildStagePayload(
|
|
1963
2154
|
'creative_director',
|
|
1964
2155
|
{
|
|
1965
2156
|
goal: parsed.goal,
|
|
@@ -1997,8 +2188,6 @@ async function creativeDirectorHandler(args) {
|
|
|
1997
2188
|
},
|
|
1998
2189
|
workflowId,
|
|
1999
2190
|
);
|
|
2000
|
-
|
|
2001
|
-
return toToolResult(payload);
|
|
2002
2191
|
}
|
|
2003
2192
|
|
|
2004
2193
|
async function pageArchitectHandler(args, stage = 'page_architect') {
|
|
@@ -2072,7 +2261,7 @@ async function pageArchitectHandler(args, stage = 'page_architect') {
|
|
|
2072
2261
|
workflowId,
|
|
2073
2262
|
);
|
|
2074
2263
|
|
|
2075
|
-
return toToolResult(payload);
|
|
2264
|
+
return toToolResult(payload, { ...parsed, projectRoot });
|
|
2076
2265
|
}
|
|
2077
2266
|
|
|
2078
2267
|
async function componentSyntaxExpertHandler(args) {
|
|
@@ -2081,10 +2270,15 @@ async function componentSyntaxExpertHandler(args) {
|
|
|
2081
2270
|
}
|
|
2082
2271
|
|
|
2083
2272
|
const parsed = syntaxSchemaParsed.parse(args);
|
|
2084
|
-
const workflowId = parsed.workflowId || randomUUID();
|
|
2085
2273
|
const projectRoot = resolveProjectRoot(parsed.projectRoot);
|
|
2086
2274
|
const catalog = await loadGuideCatalog(projectRoot);
|
|
2087
2275
|
const version = await resolveAgainstContract(projectRoot, catalog.frameworkVersion);
|
|
2276
|
+
return toToolResult(buildSyntaxPayload(catalog, version, parsed), { ...parsed, projectRoot }, { catalog, version });
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
function buildSyntaxPayload(catalog, version, parsed) {
|
|
2280
|
+
const includeCapabilities = parsed.includeCapabilities ?? parsed.detail === 'detailed';
|
|
2281
|
+
const workflowId = parsed.workflowId || randomUUID();
|
|
2088
2282
|
const provenanceBlock = provenanceBlockReason(catalog.provenance.status);
|
|
2089
2283
|
|
|
2090
2284
|
const capabilitySafe = version.status === 'match' && !provenanceBlock && CAPABILITY_ROADMAP.frameworkVersion === catalog.frameworkVersion;
|
|
@@ -2092,7 +2286,7 @@ async function componentSyntaxExpertHandler(args) {
|
|
|
2092
2286
|
const found = [];
|
|
2093
2287
|
const missing = [];
|
|
2094
2288
|
|
|
2095
|
-
for (const component of requested) {
|
|
2289
|
+
for (const [requestIndex, component] of requested.entries()) {
|
|
2096
2290
|
const guide = findGuideByName(catalog, component, false);
|
|
2097
2291
|
if (!guide) {
|
|
2098
2292
|
const skipLimit = Math.min(Math.max(SETTINGS.maxComponentSkips, 1), 20);
|
|
@@ -2101,13 +2295,19 @@ async function componentSyntaxExpertHandler(args) {
|
|
|
2101
2295
|
nearest: nearestMatches(catalog, component, skipLimit).map((row) => row.slug),
|
|
2102
2296
|
});
|
|
2103
2297
|
} else {
|
|
2104
|
-
found.push({
|
|
2298
|
+
found.push({
|
|
2299
|
+
requestIndex,
|
|
2300
|
+
...summarizeGuide(guide, { ...parsed, includeCapabilities }),
|
|
2301
|
+
...(includeCapabilities ? { capability: capabilitySafe ? CAPABILITIES_BY_SLUG.get(guide.slug) ?? null : null } : {}),
|
|
2302
|
+
});
|
|
2105
2303
|
}
|
|
2106
2304
|
}
|
|
2107
2305
|
|
|
2108
|
-
|
|
2306
|
+
return buildStagePayload(
|
|
2109
2307
|
'component_syntax_expert',
|
|
2110
2308
|
{
|
|
2309
|
+
detail: parsed.detail,
|
|
2310
|
+
includeCapabilities,
|
|
2111
2311
|
foundCount: found.length,
|
|
2112
2312
|
foundations: capabilitySafe ? parsed.foundations.map((slug) => CAPABILITIES_BY_SLUG.get(slug)) : [],
|
|
2113
2313
|
capabilityEvidence: { status: capabilitySafe ? 'bundled-review-snapshot' : 'blocked', basis: CAPABILITY_ROADMAP.basis, browserRun: capabilitySafe ? CAPABILITY_ROADMAP.browserRun : null },
|
|
@@ -2142,8 +2342,74 @@ async function componentSyntaxExpertHandler(args) {
|
|
|
2142
2342
|
},
|
|
2143
2343
|
workflowId,
|
|
2144
2344
|
);
|
|
2345
|
+
}
|
|
2145
2346
|
|
|
2146
|
-
|
|
2347
|
+
async function componentCatalogHandler(args) {
|
|
2348
|
+
const parsed = catalogSchemaParsed.parse(args);
|
|
2349
|
+
const projectRoot = parsed.projectRoot === undefined ? undefined : path.resolve(parsed.projectRoot);
|
|
2350
|
+
const catalog = await loadGuideCatalog(projectRoot);
|
|
2351
|
+
const version = projectRoot === undefined ? null : await resolveAgainstContract(projectRoot, catalog.frameworkVersion);
|
|
2352
|
+
return toToolResult(buildCatalogPayload(catalog, version, parsed), parsed, { catalog, version });
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function buildCatalogPayload(catalog, version, parsed) {
|
|
2356
|
+
const provenanceBlock = provenanceBlockReason(catalog.provenance.status);
|
|
2357
|
+
let entries = [...catalog.components.values()].map((guide) => {
|
|
2358
|
+
const decision = COMPONENT_DECISIONS_BY_SLUG.get(guide.slug);
|
|
2359
|
+
return {
|
|
2360
|
+
slug: guide.slug,
|
|
2361
|
+
title: guide.title,
|
|
2362
|
+
description: [...(decision?.useWhen ?? []), ...(decision?.jobs ?? [])].find((value) => typeof value === 'string' && value.trim()) ?? null,
|
|
2363
|
+
aliases: decision?.aliases ?? [],
|
|
2364
|
+
runtime: decision?.runtime ?? null,
|
|
2365
|
+
docs: guide.sourceUrl,
|
|
2366
|
+
};
|
|
2367
|
+
}).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
2368
|
+
|
|
2369
|
+
let search = {};
|
|
2370
|
+
if (parsed.query !== undefined) {
|
|
2371
|
+
const query = normalizeForMatch(parsed.query);
|
|
2372
|
+
const tokens = query.split('-').filter(Boolean);
|
|
2373
|
+
const matchOrder = ['exact-name', 'exact-alias', 'heuristic'];
|
|
2374
|
+
entries = entries.flatMap((entry) => {
|
|
2375
|
+
const names = [entry.slug, entry.title].map(normalizeForMatch);
|
|
2376
|
+
const aliases = entry.aliases.map(normalizeForMatch);
|
|
2377
|
+
const fields = [...names, ...aliases, normalizeForMatch(entry.description)];
|
|
2378
|
+
const matchType = query && names.includes(query) ? 'exact-name'
|
|
2379
|
+
: query && aliases.includes(query) ? 'exact-alias'
|
|
2380
|
+
: tokens.length && tokens.every((token) => fields.some((field) => field.includes(token))) ? 'heuristic' : null;
|
|
2381
|
+
return matchType ? [{ ...entry, matchType }] : [];
|
|
2382
|
+
}).sort((a, b) => matchOrder.indexOf(a.matchType) - matchOrder.indexOf(b.matchType) || a.slug.localeCompare(b.slug));
|
|
2383
|
+
const totalMatches = entries.length;
|
|
2384
|
+
const limit = parsed.limit ?? 10;
|
|
2385
|
+
entries = entries.slice(0, limit);
|
|
2386
|
+
const omittedCount = totalMatches - entries.length;
|
|
2387
|
+
search = { query: parsed.query, limit, totalMatches, omittedCount, truncated: omittedCount > 0 };
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
return buildStagePayload('component_catalog', {
|
|
2391
|
+
entries,
|
|
2392
|
+
count: entries.length,
|
|
2393
|
+
...search,
|
|
2394
|
+
contractVersion: catalog.frameworkVersion,
|
|
2395
|
+
sourceHash: catalog.sourceHash,
|
|
2396
|
+
guideSource: catalog.guideSource,
|
|
2397
|
+
status: (!version || version.status === 'match') && !provenanceBlock ? 'available' : 'blocked',
|
|
2398
|
+
checksPerformed: [parsed.query === undefined ? 'bundled component catalogue listing' : 'bundled component catalogue search', ...(version ? ['target contract resolution', 'contract provenance validation'] : [])],
|
|
2399
|
+
evidenceSources: ['bundled:component-guides.json', 'bundled:component-decisions.json', 'bundled:contract.json', ...(version ? [version.resolutionSource] : [])],
|
|
2400
|
+
uncheckedAreas: [
|
|
2401
|
+
...(!version ? ['target-project compatibility', 'target-project provenance'] : []),
|
|
2402
|
+
'rendered component behavior', 'visual hierarchy', 'responsive composition', 'keyboard and assistive-technology behavior',
|
|
2403
|
+
],
|
|
2404
|
+
contractCompatibility: version?.status ?? 'unknown',
|
|
2405
|
+
contractProvenance: catalog.provenance.status,
|
|
2406
|
+
contractProvenanceDetails: catalog.provenance,
|
|
2407
|
+
coverageStatus: parsed.query === undefined ? 'complete-bundled-catalogue' : search.truncated ? 'partial-search-results' : 'complete-search-results',
|
|
2408
|
+
blockedChecks: [
|
|
2409
|
+
...(version && version.status !== 'match' ? ['target-version contract checks'] : []),
|
|
2410
|
+
...(provenanceBlock ? [provenanceBlock] : []),
|
|
2411
|
+
],
|
|
2412
|
+
}, parsed.workflowId);
|
|
2147
2413
|
}
|
|
2148
2414
|
|
|
2149
2415
|
async function qualityInspectorHandler(args) {
|
|
@@ -2315,6 +2581,105 @@ async function qualityInspectorHandler(args) {
|
|
|
2315
2581
|
return toToolResult(payload);
|
|
2316
2582
|
}
|
|
2317
2583
|
|
|
2584
|
+
const COMPONENT_SECTIONS = ['contract', 'syntax', 'rules', 'options', 'methods'];
|
|
2585
|
+
|
|
2586
|
+
function resourceRequiredBytes(result) {
|
|
2587
|
+
// A retry's budget field can need more decimal digits. Include those bytes too.
|
|
2588
|
+
const retry = structuredClone(result);
|
|
2589
|
+
const payload = JSON.parse(retry.contents[0].text);
|
|
2590
|
+
let requiredBytes = Buffer.byteLength(JSON.stringify(retry), 'utf8');
|
|
2591
|
+
for (;;) {
|
|
2592
|
+
payload.responseBudget.maxBytes = requiredBytes;
|
|
2593
|
+
retry.contents[0].text = JSON.stringify(payload);
|
|
2594
|
+
const measured = Buffer.byteLength(JSON.stringify(retry), 'utf8');
|
|
2595
|
+
if (measured <= requiredBytes) return requiredBytes;
|
|
2596
|
+
requiredBytes = measured;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
function resourceBudgetError(uri, result, resources) {
|
|
2601
|
+
const requiredBytes = resourceRequiredBytes(result);
|
|
2602
|
+
const data = { uri, maxBytes: SETTINGS.maxResponseBytes, requiredBytes, setting: RESPONSE_BUDGET_SETTING };
|
|
2603
|
+
if (!resources.sections.has(uri)) {
|
|
2604
|
+
return {
|
|
2605
|
+
code: -32001,
|
|
2606
|
+
message: `Response budget cannot fit the complete catalogue resource. Increase ${RESPONSE_BUDGET_SETTING} to at least ${requiredBytes} and restart.`,
|
|
2607
|
+
data,
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
// Recover a whole guide through the section URIs whose complete reads fit this budget.
|
|
2611
|
+
const fits = (sectionUri) => Buffer.byteLength(JSON.stringify(resources.readers.get(sectionUri)()), 'utf8') <= SETTINGS.maxResponseBytes;
|
|
2612
|
+
const sections = resources.sections.get(uri);
|
|
2613
|
+
const recoveries = sections.length > 1 ? sections.filter(fits) : [];
|
|
2614
|
+
return {
|
|
2615
|
+
code: -32001,
|
|
2616
|
+
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.`,
|
|
2617
|
+
data: {
|
|
2618
|
+
...data,
|
|
2619
|
+
recoveries,
|
|
2620
|
+
unrecoverableSections: sections.filter((sectionUri) => !recoveries.includes(sectionUri)).map((sectionUri) => sectionUri.slice(sectionUri.lastIndexOf('/') + 1)),
|
|
2621
|
+
},
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
function bundledResources(catalog, catalogUri, catalogSnapshot) {
|
|
2626
|
+
const base = catalogUri.replace('expressivecss://catalogue/', 'expressivecss://components/');
|
|
2627
|
+
const jsonResource = (uri, payload) => () => ({
|
|
2628
|
+
contents: [{
|
|
2629
|
+
uri,
|
|
2630
|
+
mimeType: 'application/json',
|
|
2631
|
+
text: JSON.stringify({
|
|
2632
|
+
...payload,
|
|
2633
|
+
responseBudget: { maxBytes: SETTINGS.maxResponseBytes, delivery: 'complete', omissions: [], recoveries: [] },
|
|
2634
|
+
}),
|
|
2635
|
+
}],
|
|
2636
|
+
});
|
|
2637
|
+
const readers = new Map([[catalogUri, jsonResource(catalogUri, { schemaVersion: 1, ...catalogSnapshot })]]);
|
|
2638
|
+
// Whole-guide and section URIs map to the section URIs that can recover them.
|
|
2639
|
+
const sections = new Map();
|
|
2640
|
+
const guides = [];
|
|
2641
|
+
const provenanceBlock = provenanceBlockReason(catalog.provenance.status);
|
|
2642
|
+
for (const guide of [...catalog.components.values()].sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
2643
|
+
const uri = `${base}/${guide.slug}`;
|
|
2644
|
+
const identity = {
|
|
2645
|
+
schemaVersion: 1,
|
|
2646
|
+
slug: guide.slug,
|
|
2647
|
+
title: guide.title,
|
|
2648
|
+
docs: guide.sourceUrl,
|
|
2649
|
+
repositorySource: guide.astroSource,
|
|
2650
|
+
contractVersion: catalog.frameworkVersion,
|
|
2651
|
+
sourceHash: catalog.sourceHash,
|
|
2652
|
+
guideSource: catalog.guideSource,
|
|
2653
|
+
};
|
|
2654
|
+
const fields = {
|
|
2655
|
+
contract: guide.contract,
|
|
2656
|
+
syntax: { language: guide.syntax.language, example: guide.syntax.code },
|
|
2657
|
+
rules: guide.rules,
|
|
2658
|
+
options: { status: guide.options ? 'documented' : 'absent', markdown: guide.options },
|
|
2659
|
+
methods: { status: guide.methods ? 'documented' : 'absent', markdown: guide.methods },
|
|
2660
|
+
};
|
|
2661
|
+
const evidence = (coverageStatus) => ({
|
|
2662
|
+
status: provenanceBlock ? 'blocked' : 'available',
|
|
2663
|
+
checksPerformed: ['bundled component guide lookup'],
|
|
2664
|
+
evidenceSources: [`${catalog.guideSource}:${guide.file}`, 'bundled:contract.json'],
|
|
2665
|
+
uncheckedAreas: catalogSnapshot.uncheckedAreas,
|
|
2666
|
+
contractCompatibility: 'unknown',
|
|
2667
|
+
contractProvenance: catalog.provenance.status,
|
|
2668
|
+
contractProvenanceDetails: catalog.provenance,
|
|
2669
|
+
coverageStatus,
|
|
2670
|
+
blockedChecks: provenanceBlock ? [provenanceBlock] : [],
|
|
2671
|
+
});
|
|
2672
|
+
readers.set(uri, jsonResource(uri, { ...identity, ...fields, ...evidence('complete-bundled-guide') }));
|
|
2673
|
+
guides.push(guide);
|
|
2674
|
+
sections.set(uri, COMPONENT_SECTIONS.map((name) => `${uri}/${name}`));
|
|
2675
|
+
for (const name of COMPONENT_SECTIONS) {
|
|
2676
|
+
readers.set(`${uri}/${name}`, jsonResource(`${uri}/${name}`, { ...identity, section: name, [name]: fields[name], ...evidence('complete-bundled-guide-section') }));
|
|
2677
|
+
sections.set(`${uri}/${name}`, [`${uri}/${name}`]);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return { base, readers, sections, guides };
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2318
2683
|
async function startServer() {
|
|
2319
2684
|
const server = new McpServer(
|
|
2320
2685
|
{
|
|
@@ -2329,9 +2694,9 @@ async function startServer() {
|
|
|
2329
2694
|
);
|
|
2330
2695
|
|
|
2331
2696
|
server.registerTool('setup_expert', {
|
|
2332
|
-
description: TOOL_DESCRIPTIONS.setup_expert.description,
|
|
2697
|
+
description: TOOL_DESCRIPTIONS.setup_expert.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2333
2698
|
inputSchema: setupSchema,
|
|
2334
|
-
outputSchema:
|
|
2699
|
+
outputSchema: guidanceOutputSchema,
|
|
2335
2700
|
annotations: readAnnotations,
|
|
2336
2701
|
}, setupExpertHandler);
|
|
2337
2702
|
|
|
@@ -2343,33 +2708,40 @@ async function startServer() {
|
|
|
2343
2708
|
}, rulesEnforcerHandler);
|
|
2344
2709
|
|
|
2345
2710
|
server.registerTool('creative_director', {
|
|
2346
|
-
description: TOOL_DESCRIPTIONS.creative_director.description,
|
|
2711
|
+
description: TOOL_DESCRIPTIONS.creative_director.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2347
2712
|
inputSchema: creativeSchema,
|
|
2348
|
-
outputSchema:
|
|
2713
|
+
outputSchema: guidanceOutputSchema,
|
|
2349
2714
|
annotations: readAnnotations,
|
|
2350
2715
|
}, creativeDirectorHandler);
|
|
2351
2716
|
|
|
2352
2717
|
server.registerTool('page_architect', {
|
|
2353
|
-
description: TOOL_DESCRIPTIONS.page_architect.description,
|
|
2718
|
+
description: TOOL_DESCRIPTIONS.page_architect.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2354
2719
|
inputSchema: pageArchitectSchema,
|
|
2355
|
-
outputSchema:
|
|
2720
|
+
outputSchema: guidanceOutputSchema,
|
|
2356
2721
|
annotations: readAnnotations,
|
|
2357
2722
|
}, (args) => pageArchitectHandler(args, 'page_architect'));
|
|
2358
2723
|
|
|
2359
2724
|
server.registerTool('page_arcjitect', {
|
|
2360
|
-
description: TOOL_DESCRIPTIONS.page_arcjitect.description,
|
|
2725
|
+
description: TOOL_DESCRIPTIONS.page_arcjitect.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2361
2726
|
inputSchema: pageArchitectSchema,
|
|
2362
|
-
outputSchema:
|
|
2727
|
+
outputSchema: guidanceOutputSchema,
|
|
2363
2728
|
annotations: readAnnotations,
|
|
2364
2729
|
}, (args) => pageArchitectHandler(args, 'page_arcjitect'));
|
|
2365
2730
|
|
|
2366
2731
|
server.registerTool('component_syntax_expert', {
|
|
2367
|
-
description: TOOL_DESCRIPTIONS.component_syntax_expert.description,
|
|
2732
|
+
description: TOOL_DESCRIPTIONS.component_syntax_expert.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2368
2733
|
inputSchema: syntaxSchema,
|
|
2369
|
-
outputSchema:
|
|
2734
|
+
outputSchema: syntaxOutputSchema,
|
|
2370
2735
|
annotations: readAnnotations,
|
|
2371
2736
|
}, componentSyntaxExpertHandler);
|
|
2372
2737
|
|
|
2738
|
+
server.registerTool('component_catalog', {
|
|
2739
|
+
description: TOOL_DESCRIPTIONS.component_catalog.description + " Responses share the operator byte budget with explicit whole-unit omissions and recovery.",
|
|
2740
|
+
inputSchema: catalogSchema,
|
|
2741
|
+
outputSchema: guidanceOutputSchema,
|
|
2742
|
+
annotations: readAnnotations,
|
|
2743
|
+
}, componentCatalogHandler);
|
|
2744
|
+
|
|
2373
2745
|
server.registerTool('quality_inspector', {
|
|
2374
2746
|
description: TOOL_DESCRIPTIONS.quality_inspector.description,
|
|
2375
2747
|
inputSchema: inspectSchema,
|
|
@@ -2377,7 +2749,75 @@ async function startServer() {
|
|
|
2377
2749
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2378
2750
|
}, qualityInspectorHandler);
|
|
2379
2751
|
|
|
2752
|
+
const catalog = await loadGuideCatalog(undefined);
|
|
2753
|
+
const catalogUri = `expressivecss://catalogue/${encodeURIComponent(catalog.frameworkVersion)}/${catalog.sourceHash}`;
|
|
2754
|
+
const { stage, workflowId, ...snapshot } = buildCatalogPayload(catalog, null, { workflowId: 'catalogue-resource' });
|
|
2755
|
+
const resources = bundledResources(catalog, catalogUri, snapshot);
|
|
2756
|
+
const readResource = async (uri) => {
|
|
2757
|
+
const reader = resources.readers.get(uri);
|
|
2758
|
+
if (!reader) throw new McpError(-32002, 'Resource not found. Use resources/list or resources/templates/list to discover the current bundled snapshot.');
|
|
2759
|
+
return reader();
|
|
2760
|
+
};
|
|
2761
|
+
server.registerResource('component_catalog', catalogUri, {
|
|
2762
|
+
title: 'ExpressiveCSS component catalogue',
|
|
2763
|
+
description: 'Compact component metadata for the current bundled snapshot only. Consumer compatibility remains unchecked.',
|
|
2764
|
+
mimeType: 'application/json',
|
|
2765
|
+
}, () => readResource(catalogUri));
|
|
2766
|
+
// Case-insensitive prefix completion over canonical names only; the SDK caps values at 100.
|
|
2767
|
+
const startingWith = (names) => (value) => names.filter((name) => name.startsWith(value.toLowerCase()));
|
|
2768
|
+
const slug = startingWith(resources.guides.map((guide) => guide.slug));
|
|
2769
|
+
server.registerResource('component_guide', new ResourceTemplate(`${resources.base}/{slug}`, {
|
|
2770
|
+
list: async () => ({
|
|
2771
|
+
resources: resources.guides.map((guide) => ({ uri: `${resources.base}/${guide.slug}`, name: guide.slug, title: `ExpressiveCSS ${guide.title} guide` })),
|
|
2772
|
+
}),
|
|
2773
|
+
complete: { slug },
|
|
2774
|
+
}), {
|
|
2775
|
+
title: 'ExpressiveCSS component guide',
|
|
2776
|
+
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.',
|
|
2777
|
+
mimeType: 'application/json',
|
|
2778
|
+
}, (uri) => readResource(uri.href));
|
|
2779
|
+
server.registerResource('component_guide_section', new ResourceTemplate(`${resources.base}/{slug}/{section}`, {
|
|
2780
|
+
list: undefined,
|
|
2781
|
+
complete: { slug, section: startingWith(COMPONENT_SECTIONS) },
|
|
2782
|
+
}), {
|
|
2783
|
+
title: 'ExpressiveCSS component guide section',
|
|
2784
|
+
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.`,
|
|
2785
|
+
mimeType: 'application/json',
|
|
2786
|
+
}, (uri) => readResource(uri.href));
|
|
2787
|
+
// SDK v1 normalizes URLs and uses -32602 for missing resources. Require an
|
|
2788
|
+
// advertised identity verbatim and return the resource-not-found protocol code.
|
|
2789
|
+
server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => readResource(request.params.uri));
|
|
2790
|
+
|
|
2380
2791
|
const transport = new StdioServerTransport();
|
|
2792
|
+
// Check the wire result after SDK error handling too. SDK exceptions otherwise become
|
|
2793
|
+
// unbounded isError results, including validation errors before our handlers run.
|
|
2794
|
+
const scopedRequests = new Map();
|
|
2795
|
+
const start = transport.start.bind(transport);
|
|
2796
|
+
const send = transport.send.bind(transport);
|
|
2797
|
+
transport.start = async () => {
|
|
2798
|
+
const receive = transport.onmessage;
|
|
2799
|
+
transport.onmessage = (message, extra) => {
|
|
2800
|
+
if (Object.hasOwn(message, 'id')) {
|
|
2801
|
+
if (message.method === 'tools/call' && GUIDANCE_TOOLS.has(message.params?.name)) scopedRequests.set(message.id, 'tool');
|
|
2802
|
+
if (message.method === 'resources/read') scopedRequests.set(message.id, { uri: message.params?.uri });
|
|
2803
|
+
}
|
|
2804
|
+
receive(message, extra);
|
|
2805
|
+
};
|
|
2806
|
+
await start();
|
|
2807
|
+
};
|
|
2808
|
+
transport.send = async (message, options) => {
|
|
2809
|
+
const kind = scopedRequests.get(message.id);
|
|
2810
|
+
scopedRequests.delete(message.id);
|
|
2811
|
+
if (kind && Object.hasOwn(message, 'result')
|
|
2812
|
+
&& Buffer.byteLength(JSON.stringify(message.result), 'utf8') > SETTINGS.maxResponseBytes) {
|
|
2813
|
+
const error = kind.uri ? resourceBudgetError(kind.uri, message.result, resources)
|
|
2814
|
+
: { code: -32001, message: `Response budget cannot fit a truthful tool result. Increase ${RESPONSE_BUDGET_SETTING}.` };
|
|
2815
|
+
message = { jsonrpc: '2.0', id: message.id, error };
|
|
2816
|
+
}
|
|
2817
|
+
await send(message, options);
|
|
2818
|
+
};
|
|
2819
|
+
const close = transport.close.bind(transport);
|
|
2820
|
+
transport.close = async () => { try { await close(); } finally { scopedRequests.clear(); } };
|
|
2381
2821
|
await server.connect(transport);
|
|
2382
2822
|
console.error('ExpressiveCSS MCP server running on stdio transport.');
|
|
2383
2823
|
}
|