@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/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +15 -0
- package/capability-roadmap.json +1908 -221
- package/component-decisions.json +4406 -667
- package/component-guides.json +158 -42
- package/contract.json +6 -4
- package/package.json +2 -1
- package/semantics-data.json +601 -3
- package/server.js +4 -2
- package/smoke.mjs +18 -1
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 {
|
|
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:
|
|
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
|
-
|
|
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');
|