@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/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 { JSDOM } from 'jsdom';
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: /\bcard-content\b/g,
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 and constraints, plus scoped Material capability evidence. Request typography, shape, or motion through foundations.',
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 start = text.indexOf(marker);
292
- if (start === -1) {
293
- return '';
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
- const body = text.slice(start + marker.length);
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 toToolResult(payload) {
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: clampText(contract, 1_000),
590
+ contract,
417
591
  syntax: {
418
592
  language: syntaxLanguage,
419
- code: clampText(syntaxCode, Math.max(1_200, Math.floor(SETTINGS.maxComponentResponseChars / 2))),
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.resolutionSource === 'framework-source';
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
- contract: clampText(guide.contract, 900),
1289
- syntax: {
1290
- language: guide.syntax.language,
1291
- example: clampText(guide.syntax.code, 3_000),
1292
- },
1293
- rules: guide.rules.slice(0, 8),
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
- const payload = buildStagePayload(
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({ ...summarizeGuide(guide), capability: capabilitySafe ? CAPABILITIES_BY_SLUG.get(guide.slug) ?? null : null });
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
- const payload = buildStagePayload(
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
- return toToolResult(payload);
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: stageOutputSchema,
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: stageOutputSchema,
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: stageOutputSchema,
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: stageOutputSchema,
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: stageOutputSchema,
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
  }