@expressivecss/mcp-server 0.3.0 → 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,7 +5,7 @@ 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
10
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -104,7 +104,7 @@ const LegacyPatternList = [
104
104
  id: 'legacy-card-content',
105
105
  severity: 'high',
106
106
  description: '`.card-content` was removed; use the component’s documented child structure.',
107
- pattern: /\bcard-content\b/g,
107
+ pattern: /(?<![-\w])card-content(?![-\w])/g,
108
108
  },
109
109
  {
110
110
  id: 'legacy-nav-wrapper',
@@ -1060,6 +1060,8 @@ function inspectSemanticRules(snippet, maxIssues = MAX_STATIC_ISSUES, deadline =
1060
1060
  if (markupStructureExceedsLimit(snippet)) return markInspectionTruncated([], 'markup structure limit reached');
1061
1061
  const prefix = '<!doctype html><body>';
1062
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');
1063
1065
  const dom = new JSDOM(`${prefix}${snippet}</body>`, { includeNodeLocations });
1064
1066
  const { document } = dom.window;
1065
1067
  // Elements the parser creates without a start tag have no source offset, so they report the start.
package/smoke.mjs CHANGED
@@ -220,9 +220,18 @@ async function verifyResponseBudgets(matchingDir, versionedDir) {
220
220
  const result = await call(catalogRequest);
221
221
  assert.notEqual(result.structuredContent.responseBudget.delivery, 'complete');
222
222
  });
223
- await withServer(20_000, async ({ call }) => {
223
+ // A partial listing needs room for one recovery record per omitted entry, and
224
+ // that accounting grows with the catalogue. Derive the budget from the
225
+ // complete listing so it stays below it while the accounting still fits; a
226
+ // fixed figure turns into the documented error once enough components exist.
227
+ let listingBytes;
228
+ await withServer(1_048_576, async ({ call }) => {
229
+ listingBytes = bytes(await call(request('component_catalog')));
230
+ });
231
+ await withServer(listingBytes - 3_000, async ({ call }) => {
224
232
  const listing = await call(request('component_catalog'));
225
233
  assert.equal(listing.structuredContent.responseBudget.delivery, 'partial');
234
+ assert.ok(listing.structuredContent.responseBudget.omissions.length > 1, 'the listing omits several entries');
226
235
  assert.equal(listing.structuredContent.count + listing.structuredContent.responseBudget.omissions.length, guideData.guides.length);
227
236
  for (const recovery of listing.structuredContent.responseBudget.recoveries) {
228
237
  assert.equal(recovery.action, 'retry');
@@ -230,6 +239,8 @@ async function verifyResponseBudgets(matchingDir, versionedDir) {
230
239
  assert.equal(recovered.structuredContent.entries[0].slug, recovery.request.arguments.query);
231
240
  assert.equal(recovered.structuredContent.responseBudget.delivery, 'complete');
232
241
  }
242
+ });
243
+ await withServer(20_000, async ({ call }) => {
233
244
  const duplicate = await call(request('component_syntax_expert', { projectRoot: matchingDir, components: ['cards', 'cards', 'unknown-name', 'autocomplete'], sections: ['options', 'methods'] }));
234
245
  assert.equal(duplicate.structuredContent.missing[0].requested, 'unknown-name');
235
246
  for (const omitted of duplicate.structuredContent.responseBudget.omissions.filter((row) => row.slug === 'cards')) assert.ok([0, 1].includes(omitted.index));
@@ -958,6 +969,12 @@ const transport = new StdioClientTransport({
958
969
  assert.match(long.stdout, /long\.html:1:1 nav-needs-label/u);
959
970
  assert.doesNotMatch(long.stdout, /inspection-truncated/u);
960
971
  assert.equal(lint([outsideFile]).status, 0);
972
+ // The retired class name matches only as a whole class, not inside documented names that contain it.
973
+ const cardContentMarkup = path.join(outsideDir, 'card-content.html');
974
+ await writeFile(cardContentMarkup, '<style>article { --md-comp-card-content-padding: 24px; }</style>\n<div class="expanding-card-content">Details</div>\n<div class="card-content">Old</div>\n');
975
+ const cardContent = lint([cardContentMarkup]);
976
+ assert.equal(cardContent.stdout.match(/legacy-card-content/gu)?.length, 1, cardContent.stdout);
977
+ assert.match(cardContent.stdout, /card-content\.html:3:\d+ legacy-card-content/u);
961
978
  // Files the bounded reader refuses, and inspections that stop early, fail instead of passing silently.
962
979
  const linkedMarkup = path.join(outsideDir, 'linked.html');
963
980
  const crowdedMarkup = path.join(outsideDir, 'crowded.html');