@master/css-mcp 2.1.1 → 2.1.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Master CSS MCP Server
2
2
 
3
- Model Context Protocol server for Master CSS tooling. It lets AI clients inspect a Master CSS workspace, audit setup, read the active project manifest, inspect CSS-first directives, trace classes through extraction and generated CSS, render or compare CSS output, run lint diagnostics, request syntax suggestions, and apply reviewed fix previews.
3
+ Model Context Protocol server for Master CSS tooling. It lets AI clients inspect a Master CSS workspace, audit setup, read the active project manifest, inspect CSS-first directives, trace classes through extraction and generated CSS, render or compare CSS output, run lint diagnostics, request syntax suggestions, read and check the component contracts of a design system, and apply reviewed fix previews.
4
4
 
5
5
  ## Installation
6
6
 
@@ -60,6 +60,9 @@ The `preset` context uses the bundled Master CSS defaults, so this check works b
60
60
  | `mastercss_lint_project` | Run class-list diagnostics for workspace files without writing files. |
61
61
  | `mastercss_lint_content` | Run class-list diagnostics on an in-memory source buffer without writing files. |
62
62
  | `mastercss_suggest_syntax` | Return language-service completions and hover context. |
63
+ | `mastercss_contract_list` | List the component contracts of the workspace's design-system dependencies, merged with the project's own. Filter by `query`, `tier`, or `category`. |
64
+ | `mastercss_contract_get` | Return one full contract and its source by contract name or by any class it defines. |
65
+ | `mastercss_contract_check` | Check an in-memory source buffer or workspace files against the contracts with the checker a design-system dependency declares, without writing files. |
63
66
  | `mastercss_preview_fixes` | Create a diff preview and confirmation token. |
64
67
  | `mastercss_preview_directive_format` | Preview Master CSS directive formatting for files, or format in-memory content without writing. |
65
68
  | `mastercss_apply_preview` | Apply a preview after token, hash, and workspace checks. |
@@ -92,6 +95,51 @@ The report covers project-owned CSS reached from the project's Master CSS entrie
92
95
 
93
96
  Metadata and policies are analysis inputs: they do not change generated CSS, manifests, or runtime programs. Unrecognized option fields are rejected. Omit `rawValuePolicy` to skip native declaration raw-value checks. Read `project.excluded` and `coverage`: package sources and files outside the project root are excluded, importer links include reference-only context, and the report does not establish DOM usage, cascade winners, computed styles, or browser behavior. `includeSnapshot` remains available in the same call.
94
97
 
98
+ ## Contracts of a design system
99
+
100
+ A design system built on Master CSS can describe each component in a contract: its classes and where they go, its states, public properties and smallest correct markup. The contract tools serve those contracts from the same server, so an agent can read a component's contract before writing its markup and check the result afterward. [`@master/ui`](https://ui.master.co) is the first library that ships contracts this way.
101
+
102
+ A library declares its contracts in its own `package.json`:
103
+
104
+ ```json
105
+ {
106
+ "name": "your-design-system",
107
+ "masterCSS": {
108
+ "contracts": "./dist/contracts.json",
109
+ "check": "./check/index.js"
110
+ }
111
+ }
112
+ ```
113
+
114
+ - `contracts` is a path inside the package to a JSON file holding `{ "contracts": [...] }`. A contract is an object with a string `name` and a `classes` array; every other field belongs to the library.
115
+ - `check` is optional: a path inside the package to an ES module that exports `check(source, contracts, { knows, strict })` and returns diagnostics with `code`, `severity`, `message`, `line`, `column` and, when they apply, `className` and `contract`.
116
+
117
+ The server reads `dependencies`, `devDependencies` and `peerDependencies` from the `package.json` in the workspace root, resolves each package from that root and keeps those that declare `masterCSS.contracts`. Packages that do not resolve or do not declare the field are skipped. When none declares contracts, the server reads the same fields of the monorepo root above: the nearest directory whose `package.json` lists `workspaces` or that holds a `pnpm-workspace.yaml`, without leaving the repository.
118
+
119
+ A project adds contracts of its own with globs in the same field of its root `package.json`, one contract per JSON file:
120
+
121
+ ```json
122
+ {
123
+ "masterCSS": {
124
+ "contracts": ["contracts/*.json"]
125
+ }
126
+ }
127
+ ```
128
+
129
+ A project with a `master-ui.json` also contributes the JSON files of its `directory` (`ui` by default) and its `contracts` globs. A project contract replaces a library contract of the same `name`. Every contract in a result carries its `source`: `{ "kind": "package", "package", "version" }`, or `{ "kind": "project", "file" }` with `replaces` naming the library contract it took over.
130
+
131
+ | Call | Result |
132
+ | --- | --- |
133
+ | `mastercss_contract_list` with optional `query`, `tier`, `category` | `libraries` (name, version, contract count, whether it has a checker) and a compact `contracts` list. Nothing declaring contracts is an empty list with a `note`, not an error. |
134
+ | `mastercss_contract_get` with `name` | The full `contract` and its `source`. `name` is a contract name or any class in a contract, so `btn-primary` returns the button contract. An unknown name is an error that lists the closest contract and class names. |
135
+ | `mastercss_contract_check` with `content` and optional `filePath`, or `patterns` | Diagnostics per file, a `summary`, `coverage`, and the `checkers` that ran. Without input it checks the project's markup files. |
136
+
137
+ A library in its own repository can declare `masterCSS.check` in the root `package.json` too; that checker then reads the contracts its root `masterCSS.contracts` globs select.
138
+
139
+ `mastercss_contract_check` runs code from the package that declares the checker. The server imports it only from a path inside that package, runs it in a worker thread, and stops it with `CONTRACT_CHECKER_FAILED` when one file takes longer than 10 seconds or the checker exceeds its memory limit. Each check loads the checker again, so an upgraded library takes effect without a restart.
140
+
141
+ `mastercss_contract_check` passes the checker `knows(className)`, which answers from the project manifest whether a class is a valid Master CSS class or a native class of the project's stylesheets, and `strict: true` only when that manifest loaded; `classValidation` and `strict` in the result say which happened. When contracts exist but no library declares a checker, `coverage.status` is `no-checker` and lookups still work. With several libraries, each checker reads its own library's contracts as the project replaced them, and contracts no checker owns go to the first one.
142
+
95
143
  ## Prompt templates
96
144
 
97
145
  | Prompt | Use |
@@ -106,7 +154,9 @@ Every tool returns a `version: 3` envelope with `metadata`, `diagnostics` and `r
106
154
 
107
155
  ## Safety
108
156
 
109
- Read-only tools do not write files. File writes use a two-step preview/apply flow, validate workspace containment, and verify original file hashes before writing. Lint fixes, generated CSS output writes, and directive formatting for files all use preview tokens before `mastercss_apply_preview` can write anything.
157
+ Read-only tools do not write files. `mastercss_contract_check` is the one tool that runs code from the workspace: it imports the checker module of a package that is a declared dependency of the workspace root, only from a path inside that package, and runs it in the server process. Project contract files are read as data and never executed. Restart the server after upgrading a library so that its checker is loaded again.
158
+
159
+ File writes use a two-step preview/apply flow, validate workspace containment, and verify original file hashes before writing. Lint fixes, generated CSS output writes, and directive formatting for files all use preview tokens before `mastercss_apply_preview` can write anything.
110
160
 
111
161
  Concurrent applies coordinate across MCP processes for the same OS user through a local lock directory under `~/.cache/mastercss`. Hash validation and writes run one apply at a time across all workspace roots. A waiting apply times out after 30 seconds without taking over a live writer; its token can be retried until it expires. External editors do not participate in this lock, so edits made after hash validation can still race with apply. Multi-file writes are sequential and do not provide rollback or crash atomicity.
112
162
 
@@ -0,0 +1,104 @@
1
+ import { metadata, strings } from "./semantic-schema.js";
2
+ import * as z from "zod/v4";
3
+ // src/contract-schema.ts
4
+ const count = z.number().int().nonnegative();
5
+ const owner = z.object({
6
+ package: z.string(),
7
+ version: z.string().nullable()
8
+ });
9
+ /** Where a contract comes from: the library that ships it, or the project file that defines or replaces it. */
10
+ const contractSource = z.discriminatedUnion("kind", [owner.extend({ kind: z.literal("package") }), z.object({
11
+ kind: z.literal("project"),
12
+ file: z.string(),
13
+ replaces: owner.optional()
14
+ })]);
15
+ const library = z.object({
16
+ name: z.string(),
17
+ version: z.string().nullable(),
18
+ count,
19
+ checker: z.boolean()
20
+ });
21
+ const entry = z.object({
22
+ name: z.string(),
23
+ title: z.string().optional(),
24
+ tier: z.string().optional(),
25
+ category: z.string().optional(),
26
+ description: z.string().optional(),
27
+ use: z.string().optional(),
28
+ script: z.string().optional(),
29
+ source: contractSource
30
+ });
31
+ const contract = z.object({
32
+ name: z.string(),
33
+ classes: z.array(z.json())
34
+ }).passthrough();
35
+ const diagnostic = z.object({
36
+ code: z.string(),
37
+ severity: z.enum([
38
+ "error",
39
+ "warning",
40
+ "info"
41
+ ]),
42
+ message: z.string(),
43
+ line: count,
44
+ column: count,
45
+ className: z.string().optional(),
46
+ contract: z.string().optional(),
47
+ source: z.string()
48
+ });
49
+ const contractList = {
50
+ version: z.literal(1),
51
+ root: z.string(),
52
+ libraries: z.array(library),
53
+ contracts: z.array(entry),
54
+ summary: z.object({
55
+ libraries: count,
56
+ contracts: count,
57
+ returned: count
58
+ }),
59
+ note: z.string().optional()
60
+ };
61
+ const contractGet = {
62
+ version: z.literal(1),
63
+ root: z.string(),
64
+ name: z.string(),
65
+ matched: z.enum(["contract", "class"]),
66
+ contract,
67
+ source: contractSource
68
+ };
69
+ const contractCheck = {
70
+ version: z.literal(1),
71
+ root: z.string(),
72
+ manifest: metadata.optional(),
73
+ libraries: z.array(library),
74
+ checkers: z.array(owner.extend({ contracts: count })),
75
+ classValidation: z.enum(["project-manifest", "none"]),
76
+ strict: z.boolean(),
77
+ files: z.array(z.object({
78
+ filePath: z.string(),
79
+ diagnostics: z.array(diagnostic)
80
+ })),
81
+ coverage: z.object({
82
+ status: z.enum([
83
+ "complete",
84
+ "no-inputs",
85
+ "no-checker",
86
+ "no-contracts"
87
+ ]),
88
+ checkedFiles: strings,
89
+ skippedFiles: z.array(z.object({
90
+ filePath: z.string(),
91
+ reason: z.string()
92
+ })),
93
+ description: z.string()
94
+ }),
95
+ summary: z.object({
96
+ checkedFiles: count,
97
+ filesWithDiagnostics: count,
98
+ diagnostics: count,
99
+ errors: count,
100
+ warnings: count
101
+ }),
102
+ note: z.string().optional()
103
+ };
104
+ export { contractCheck, contractGet, contractList, contractSource };
@@ -0,0 +1,623 @@
1
+ import { getErrorMessage } from "./result.js";
2
+ import { loadWorkspaceManifest, manifestMetadata } from "./project.js";
3
+ import { createMCPToolingSession } from "./tooling-session.js";
4
+ import { resolveSourceFiles } from "./scan.js";
5
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { readFile, realpath, stat } from "node:fs/promises";
8
+ import fg from "fast-glob";
9
+ import { Worker } from "node:worker_threads";
10
+ // src/contracts.ts
11
+ /**
12
+ * Contracts of a design system built on Master CSS.
13
+ *
14
+ * A package declares them in its package.json:
15
+ * "masterCSS": { "contracts": "./dist/contracts.json", "check": "./check/index.js" }
16
+ * `contracts` names a JSON file holding `{ contracts: [...] }`; the optional `check` names an ES module
17
+ * exporting `check(source, contracts, { knows, strict })`. A project adds or replaces contracts with its own
18
+ * JSON files, and a library checking itself declares its `check` in the workspace's own package.json. This module
19
+ * only reads that protocol: it knows no design system by name.
20
+ */
21
+ const CONTRACT_REPORT_VERSION = 1;
22
+ const MAX_CONTRACT_FILE_BYTES = 16777216;
23
+ const MAX_PROJECT_CONTRACT_FILES = 2e3;
24
+ const MAX_CHECK_FILES = 5e3;
25
+ const MAX_CHECK_FILE_BYTES = 2097152;
26
+ const MAX_DIAGNOSTICS_PER_FILE = 500;
27
+ const MAX_CHECK_CONTENT_LENGTH = MAX_CHECK_FILE_BYTES;
28
+ const DEFAULT_CHECK_PATTERNS = ["**/*.{html,htm,jsx,tsx,svelte,astro,vue,mdx,php}"];
29
+ const DEFAULT_IGNORE_PATTERNS = ["**/node_modules/**", "node_modules"];
30
+ const PACKAGE_NAME = /^(?:@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/i;
31
+ const NO_CONTRACTS_NOTE = "No dependency of this workspace declares \"masterCSS.contracts\" in its package.json, and the project defines no contracts of its own.";
32
+ const DEPENDENCY_FIELDS = [
33
+ "dependencies",
34
+ "devDependencies",
35
+ "peerDependencies"
36
+ ];
37
+ /** Limits of one checker call, read at every check. A checker that exceeds them is stopped, not the server. */
38
+ const contractCheckerLimits = {
39
+ timeoutMs: 1e4,
40
+ memoryMb: 512
41
+ };
42
+ const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
43
+ const text = (value) => typeof value === "string" && value ? value : void 0;
44
+ const strings = (value) => (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string" && item.length > 0);
45
+ const posix = (path) => path.replaceAll("\\", "/");
46
+ function inside(directory, filePath) {
47
+ const path = relative(directory, filePath);
48
+ return Boolean(path) && !path.startsWith("..") && !isAbsolute(path);
49
+ }
50
+ async function readJSON(filePath) {
51
+ try {
52
+ if ((await stat(filePath)).size > MAX_CONTRACT_FILE_BYTES) return {
53
+ error: `larger than ${MAX_CONTRACT_FILE_BYTES} bytes`,
54
+ missing: false
55
+ };
56
+ return { value: JSON.parse(await readFile(filePath, "utf8")) };
57
+ } catch (error) {
58
+ return {
59
+ error: getErrorMessage(error),
60
+ missing: record(error).code === "ENOENT"
61
+ };
62
+ }
63
+ }
64
+ /** A file inside a package directory, with links resolved so a path cannot leave the package. */
65
+ async function resolveInside(directory, path) {
66
+ if (isAbsolute(path)) return void 0;
67
+ try {
68
+ const filePath = await realpath(resolve(directory, path));
69
+ return inside(directory, filePath) && (await stat(filePath)).isFile() ? filePath : void 0;
70
+ } catch {
71
+ return;
72
+ }
73
+ }
74
+ /** Where Node resolves a bare package name from the root, without requiring an exported package.json. */
75
+ async function findPackageDirectory(root, name) {
76
+ for (let directory = root;; directory = dirname(directory)) {
77
+ const manifest = join(directory, "node_modules", name, "package.json");
78
+ if ((await stat(manifest).catch(() => void 0))?.isFile()) return realpath(dirname(manifest));
79
+ if (dirname(directory) === directory) return void 0;
80
+ }
81
+ }
82
+ const exists = (path) => stat(path).then(() => true, () => false);
83
+ /** The nearest ancestor that roots a workspace, without leaving the repository that holds the root. */
84
+ async function findWorkspaceRoot(root) {
85
+ if (await exists(join(root, ".git"))) return void 0;
86
+ for (let directory = dirname(root); directory !== dirname(directory); directory = dirname(directory)) {
87
+ const manifest = await readJSON(join(directory, "package.json"));
88
+ const pkg = record("value" in manifest ? manifest.value : void 0);
89
+ if (pkg.workspaces || await exists(join(directory, "pnpm-workspace.yaml"))) return {
90
+ directory,
91
+ pkg
92
+ };
93
+ if (await exists(join(directory, ".git"))) return void 0;
94
+ }
95
+ }
96
+ const isContract = (value) => typeof record(value).name === "string" && record(value).name !== "" && Array.isArray(record(value).classes);
97
+ /** One contract, or the contracts of a `{ contracts: [...] }` file. Anything else holds none. */
98
+ function contractsIn(value) {
99
+ if (isContract(value)) return [value];
100
+ const { contracts } = record(value);
101
+ return Array.isArray(contracts) ? contracts.filter(isContract) : [];
102
+ }
103
+ const classNamesOf = (contract) => contract.classes.flatMap((entry) => text(record(entry).name) ?? []);
104
+ /** The libraries among the dependencies a package.json declares, resolved from its directory. */
105
+ async function readLibraries(root, pkg, libraries, merged, warn) {
106
+ for (const dependency of new Set(DEPENDENCY_FIELDS.flatMap((field) => Object.keys(record(pkg[field]))))) {
107
+ if (!PACKAGE_NAME.test(dependency)) continue;
108
+ const directory = await findPackageDirectory(root, dependency);
109
+ if (!directory) continue;
110
+ const manifest = await readJSON(join(directory, "package.json"));
111
+ if (!("value" in manifest)) continue;
112
+ const pkg = record(manifest.value);
113
+ const declared = record(pkg.masterCSS);
114
+ const contractPaths = strings(declared.contracts);
115
+ if (!contractPaths.length) continue;
116
+ const owner = {
117
+ package: text(pkg.name) ?? dependency,
118
+ version: text(pkg.version) ?? null
119
+ };
120
+ if (libraries.some((library) => library.name === owner.package)) continue;
121
+ const library = {
122
+ name: owner.package,
123
+ version: owner.version,
124
+ count: 0
125
+ };
126
+ for (const path of contractPaths) {
127
+ const filePath = await resolveInside(directory, path);
128
+ const file = filePath ? await readJSON(filePath) : {
129
+ error: "not a file inside the package",
130
+ missing: true
131
+ };
132
+ if (!("value" in file)) {
133
+ warn("CONTRACT_FILE_UNREADABLE", `${owner.package} declares masterCSS.contracts "${path}", which could not be read: ${file.error}`);
134
+ continue;
135
+ }
136
+ for (const contract of contractsIn(file.value)) {
137
+ library.count++;
138
+ const existing = merged.get(contract.name);
139
+ if (existing) {
140
+ const other = existing.source.kind === "package" ? existing.source.package : existing.source.file;
141
+ if (other !== owner.package) warn("CONTRACT_NAME_COLLISION", `${owner.package} and ${other} both declare a contract named "${contract.name}"; ${other} is kept.`);
142
+ continue;
143
+ }
144
+ merged.set(contract.name, {
145
+ contract,
146
+ source: {
147
+ kind: "package",
148
+ ...owner
149
+ },
150
+ library: owner.package
151
+ });
152
+ }
153
+ }
154
+ const check = text(declared.check);
155
+ if (check) {
156
+ library.checkPath = await resolveInside(directory, check);
157
+ if (!library.checkPath) warn("CONTRACT_CHECKER_UNRESOLVED", `${owner.package} declares masterCSS.check "${check}", which is not a file inside the package.`);
158
+ }
159
+ libraries.push(library);
160
+ }
161
+ }
162
+ /** Read every contract that applies to the workspace. Nothing here executes package or project code. */
163
+ async function discoverContracts(context) {
164
+ const warnings = [];
165
+ const warn = (code, message) => {
166
+ warnings.push({
167
+ code,
168
+ phase: "contract-discovery",
169
+ severity: "warning",
170
+ message
171
+ });
172
+ };
173
+ const rootManifest = await readJSON(join(context.root, "package.json"));
174
+ if (!("value" in rootManifest) && !rootManifest.missing) warn("CONTRACT_PACKAGE_JSON_UNREADABLE", `The workspace package.json could not be read: ${rootManifest.error}`);
175
+ const rootPackage = record("value" in rootManifest ? rootManifest.value : void 0);
176
+ const libraries = [];
177
+ const merged = /* @__PURE__ */ new Map();
178
+ await readLibraries(context.root, rootPackage, libraries, merged, warn);
179
+ if (!libraries.length) {
180
+ const workspace = await findWorkspaceRoot(context.root);
181
+ if (workspace) await readLibraries(workspace.directory, workspace.pkg, libraries, merged, warn);
182
+ }
183
+ const patterns = strings(record(rootPackage.masterCSS).contracts);
184
+ const settingsFile = await readJSON(join(context.root, "master-ui.json"));
185
+ if ("value" in settingsFile) {
186
+ const settings = record(settingsFile.value);
187
+ patterns.push(`${text(settings.directory) ?? "ui"}/*.json`, ...strings(settings.contracts));
188
+ } else if (!settingsFile.missing) warn("CONTRACT_SETTINGS_UNREADABLE", `master-ui.json could not be read: ${settingsFile.error}`);
189
+ const accepted = [];
190
+ for (const pattern of new Set(patterns.map((pattern) => posix(pattern).replace(/^(?:\.\/)+/, "").replace(/\/{2,}/g, "/")))) try {
191
+ context.validateGlobPatterns([pattern]);
192
+ accepted.push(pattern);
193
+ } catch (error) {
194
+ warn("CONTRACT_PATTERN_REJECTED", getErrorMessage(error));
195
+ }
196
+ const files = accepted.length ? (await fg(accepted, {
197
+ cwd: context.root,
198
+ ignore: DEFAULT_IGNORE_PATTERNS,
199
+ onlyFiles: true,
200
+ followSymbolicLinks: false
201
+ })).sort() : [];
202
+ if (files.length > MAX_PROJECT_CONTRACT_FILES) {
203
+ warn("CONTRACT_FILES_TRUNCATED", `The project's contract patterns match ${files.length} files; only the first ${MAX_PROJECT_CONTRACT_FILES} are read.`);
204
+ files.length = MAX_PROJECT_CONTRACT_FILES;
205
+ }
206
+ for (const file of files) {
207
+ const filePath = await context.resolveExistingFile(file).catch(() => void 0);
208
+ const parsed = filePath ? await readJSON(filePath) : void 0;
209
+ if (!parsed || !("value" in parsed)) continue;
210
+ for (const contract of contractsIn(parsed.value)) {
211
+ const previous = merged.get(contract.name);
212
+ const replaces = previous?.source.kind === "package" ? {
213
+ package: previous.source.package,
214
+ version: previous.source.version
215
+ } : previous?.source.replaces;
216
+ merged.set(contract.name, {
217
+ contract,
218
+ source: {
219
+ kind: "project",
220
+ file: posix(file),
221
+ ...replaces ? { replaces } : {}
222
+ },
223
+ library: previous?.library
224
+ });
225
+ }
226
+ }
227
+ const ownCheck = text(record(rootPackage.masterCSS).check);
228
+ if (ownCheck) {
229
+ const checkPath = await resolveInside(await realpath(context.root), ownCheck);
230
+ if (checkPath) {
231
+ const name = text(rootPackage.name) ?? "workspace";
232
+ const own = {
233
+ name,
234
+ version: text(rootPackage.version) ?? null,
235
+ count: 0,
236
+ checkPath
237
+ };
238
+ for (const item of merged.values()) if (item.source.kind === "project" && !item.library) {
239
+ item.library = name;
240
+ own.count++;
241
+ }
242
+ libraries.unshift(own);
243
+ } else warn("CONTRACT_CHECKER_UNRESOLVED", `The workspace package.json declares masterCSS.check "${ownCheck}", which is not a file inside the workspace.`);
244
+ }
245
+ return {
246
+ libraries,
247
+ contracts: [...merged.values()],
248
+ warnings
249
+ };
250
+ }
251
+ const describeLibrary = ({ name, version, count, checkPath }) => ({
252
+ name,
253
+ version,
254
+ count,
255
+ checker: Boolean(checkPath)
256
+ });
257
+ async function listContracts(context, options = {}) {
258
+ const discovery = await discoverContracts(context);
259
+ const terms = (options.query ?? "").toLowerCase().split(/\s+/).filter(Boolean);
260
+ const same = (value, expected) => !expected?.trim() || text(value)?.toLowerCase() === expected.trim().toLowerCase();
261
+ const matched = discovery.contracts.filter(({ contract }) => {
262
+ if (!same(contract.tier, options.tier) || !same(contract.category, options.category)) return false;
263
+ const searched = [
264
+ contract.name,
265
+ text(contract.title),
266
+ text(contract.description),
267
+ text(contract.use),
268
+ ...classNamesOf(contract)
269
+ ].join("\n").toLowerCase();
270
+ return terms.every((term) => searched.includes(term));
271
+ });
272
+ return {
273
+ version: CONTRACT_REPORT_VERSION,
274
+ root: context.root,
275
+ libraries: discovery.libraries.map(describeLibrary),
276
+ contracts: matched.map(({ contract, source }) => ({
277
+ name: contract.name,
278
+ title: text(contract.title),
279
+ tier: text(contract.tier),
280
+ category: text(contract.category),
281
+ description: text(contract.description),
282
+ use: text(contract.use),
283
+ script: text(contract.script),
284
+ source
285
+ })),
286
+ diagnostics: discovery.warnings,
287
+ summary: {
288
+ libraries: discovery.libraries.length,
289
+ contracts: discovery.contracts.length,
290
+ returned: matched.length
291
+ },
292
+ ...discovery.contracts.length ? {} : { note: NO_CONTRACTS_NOTE }
293
+ };
294
+ }
295
+ function editDistance(a, b) {
296
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
297
+ for (let i = 1; i <= a.length; i++) {
298
+ const current = [i];
299
+ for (let j = 1; j <= b.length; j++) current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
300
+ previous = current;
301
+ }
302
+ return previous[b.length];
303
+ }
304
+ /** The names nearest to what was asked: one that contains it first, then a near spelling or a name it contains. */
305
+ function closest(target, candidates, limit = 5) {
306
+ const wanted = target.toLowerCase();
307
+ return candidates.map((candidate) => {
308
+ const name = candidate.toLowerCase();
309
+ const gap = Math.abs(name.length - wanted.length) / 100;
310
+ return {
311
+ candidate,
312
+ distance: name.includes(wanted) ? gap : Math.min(editDistance(wanted, name), wanted.includes(name) ? 1 + gap : Infinity)
313
+ };
314
+ }).sort((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate)).slice(0, limit).map((item) => item.candidate);
315
+ }
316
+ async function getContract(context, options) {
317
+ const discovery = await discoverContracts(context);
318
+ const name = options.name.trim();
319
+ const byClass = /* @__PURE__ */ new Map();
320
+ for (const item of discovery.contracts) for (const className of classNamesOf(item.contract)) if (!byClass.has(className)) byClass.set(className, item);
321
+ const named = discovery.contracts.find((item) => item.contract.name === name);
322
+ const found = named ?? byClass.get(name);
323
+ if (!found) {
324
+ const classes = closest(name, [...byClass.keys()]);
325
+ const contracts = [.../* @__PURE__ */ new Set([...classes.slice(0, 2).map((className) => byClass.get(className).contract.name), ...closest(name, discovery.contracts.map((item) => item.contract.name))])].slice(0, 5);
326
+ const missing = `No contract is named "${name}" and no contract has a class "${name}".`;
327
+ const lookup = (message, alternatives) => ({
328
+ code: "CONTRACT_NOT_FOUND",
329
+ phase: "contract-lookup",
330
+ severity: "info",
331
+ message,
332
+ alternatives
333
+ });
334
+ throw Object.assign(/* @__PURE__ */ new Error(discovery.contracts.length ? `${missing} Closest contracts: ${contracts.join(", ")}. Closest classes: ${classes.join(", ")}. Call mastercss_contract_list for every contract.` : `${missing} ${NO_CONTRACTS_NOTE}`), {
335
+ code: "CONTRACT_NOT_FOUND",
336
+ diagnostics: [
337
+ ...contracts.length ? [lookup("Closest contract names.", contracts)] : [],
338
+ ...classes.length ? [lookup("Closest class names.", classes)] : [],
339
+ ...discovery.warnings
340
+ ]
341
+ });
342
+ }
343
+ return {
344
+ version: CONTRACT_REPORT_VERSION,
345
+ root: context.root,
346
+ name,
347
+ matched: named ? "contract" : "class",
348
+ contract: found.contract,
349
+ source: found.source,
350
+ diagnostics: discovery.warnings
351
+ };
352
+ }
353
+ const checkerFailure = (library, action, error) => Object.assign(/* @__PURE__ */ new Error(`The contract checker of ${library.name} ${action}: ${getErrorMessage(error)}`), { code: "CONTRACT_CHECKER_FAILED" });
354
+ /**
355
+ * The worker that runs one checker. It imports the module afresh, so an upgraded library needs no restart, and
356
+ * the server can stop a checker that hangs or exhausts its memory. `knows` stays in the server: the worker asks
357
+ * for each class it has not seen and waits for the answer, so a checker still calls it synchronously.
358
+ */
359
+ const CHECKER_WORKER = `
360
+ const { parentPort, workerData } = require('node:worker_threads')
361
+ const { checkURL, contracts, signal } = workerData
362
+ const answers = new Map()
363
+ function knows(className) {
364
+ const key = String(className)
365
+ if (!answers.has(key)) {
366
+ Atomics.store(signal, 0, 0)
367
+ parentPort.postMessage({ type: 'knows', className: key })
368
+ Atomics.wait(signal, 0, 0)
369
+ answers.set(key, signal[1] === 1)
370
+ }
371
+ return answers.get(key)
372
+ }
373
+ const describe = error => error instanceof Error ? error.message : String(error)
374
+ const loading = import(checkURL).then(module => {
375
+ if (typeof module.check !== 'function') throw new Error('the module does not export a check function')
376
+ return module.check
377
+ })
378
+ loading.then(() => parentPort.postMessage({ type: 'ready' }), error => parentPort.postMessage({ type: 'failed', message: describe(error) }))
379
+ parentPort.on('message', async ({ content, strict, withKnows }) => {
380
+ try {
381
+ const reported = await (await loading)(content, contracts, withKnows ? { knows, strict } : { strict })
382
+ parentPort.postMessage(Array.isArray(reported)
383
+ ? { type: 'result', diagnostics: JSON.parse(JSON.stringify(reported.slice(0, ${MAX_DIAGNOSTICS_PER_FILE}))) }
384
+ : { type: 'failed', message: 'check() did not return an array of diagnostics' })
385
+ } catch (error) {
386
+ parentPort.postMessage({ type: 'failed', message: describe(error) })
387
+ }
388
+ })
389
+ `;
390
+ /** Start the checker a library ships. Discovery already proved the file is inside that library. */
391
+ async function startChecker(library, contracts, knows) {
392
+ const { timeoutMs, memoryMb } = contractCheckerLimits;
393
+ const signal = new Int32Array(new SharedArrayBuffer(8));
394
+ const worker = new Worker(CHECKER_WORKER, {
395
+ eval: true,
396
+ stdout: true,
397
+ workerData: {
398
+ checkURL: pathToFileURL(library.checkPath).href,
399
+ contracts,
400
+ signal
401
+ },
402
+ resourceLimits: { maxOldGenerationSizeMb: memoryMb }
403
+ });
404
+ worker.stdout.pipe(process.stderr, { end: false });
405
+ let waiting;
406
+ let ended;
407
+ const end = (error) => {
408
+ ended ??= error;
409
+ waiting?.reject(ended);
410
+ waiting = void 0;
411
+ };
412
+ worker.on("error", end);
413
+ worker.on("exit", (code) => end(/* @__PURE__ */ new Error(`its worker stopped with exit code ${code}`)));
414
+ worker.on("message", (message) => {
415
+ if (message.type === "knows") {
416
+ signal[1] = knows?.(String(message.className)) ? 1 : 0;
417
+ Atomics.store(signal, 0, 1);
418
+ Atomics.notify(signal, 0);
419
+ return;
420
+ }
421
+ const current = waiting;
422
+ waiting = void 0;
423
+ if (message.type === "failed") current?.reject(new Error(String(message.message)));
424
+ else current?.resolve(message);
425
+ });
426
+ /** The worker's next answer, or the reason it gave none within the time limit. */
427
+ const answer = (action, send) => new Promise((resolve, reject) => {
428
+ if (ended) return reject(ended);
429
+ const timer = setTimeout(() => {
430
+ end(/* @__PURE__ */ new Error(`did not finish within ${timeoutMs} ms`));
431
+ worker.terminate();
432
+ }, timeoutMs);
433
+ waiting = {
434
+ resolve: (message) => {
435
+ clearTimeout(timer);
436
+ resolve(message);
437
+ },
438
+ reject: (error) => {
439
+ clearTimeout(timer);
440
+ reject(error);
441
+ }
442
+ };
443
+ send?.();
444
+ }).catch((error) => {
445
+ throw checkerFailure(library, action, error);
446
+ });
447
+ const dispose = async () => {
448
+ ended ??= /* @__PURE__ */ new Error("its worker was stopped");
449
+ await worker.terminate();
450
+ };
451
+ try {
452
+ await answer("could not be loaded");
453
+ } catch (error) {
454
+ await dispose();
455
+ throw error;
456
+ }
457
+ return {
458
+ check: async (content, strict, filePath) => (await answer(`failed on ${filePath}`, () => worker.postMessage({
459
+ content,
460
+ strict,
461
+ withKnows: Boolean(knows)
462
+ }))).diagnostics,
463
+ dispose
464
+ };
465
+ }
466
+ function normalizeDiagnostic(value, source) {
467
+ const diagnostic = record(value);
468
+ if (typeof diagnostic.message !== "string") return [];
469
+ const position = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
470
+ const className = text(diagnostic.className);
471
+ const contract = text(diagnostic.contract);
472
+ return [{
473
+ code: text(diagnostic.code) ?? "contract",
474
+ severity: diagnostic.severity === "warning" || diagnostic.severity === "info" ? diagnostic.severity : "error",
475
+ message: diagnostic.message,
476
+ line: position(diagnostic.line),
477
+ column: position(diagnostic.column),
478
+ ...className ? { className } : {},
479
+ ...contract ? { contract } : {},
480
+ source
481
+ }];
482
+ }
483
+ async function readCheckInputs(context, options) {
484
+ const skippedFiles = [];
485
+ if (options.content !== void 0) return {
486
+ inputs: [{
487
+ filePath: context.resolveVirtualPath(options.filePath || "index.html"),
488
+ content: options.content
489
+ }],
490
+ skippedFiles
491
+ };
492
+ const filePaths = (await resolveSourceFiles(context, options.patterns, options.patterns ? [] : DEFAULT_IGNORE_PATTERNS, DEFAULT_CHECK_PATTERNS)).sort();
493
+ if (filePaths.length > MAX_CHECK_FILES) throw Object.assign(/* @__PURE__ */ new Error(`The patterns match ${filePaths.length} files; a contract check reads at most ${MAX_CHECK_FILES}. Pass narrower patterns.`), { code: "CONTRACT_CHECK_TOO_MANY_FILES" });
494
+ const inputs = [];
495
+ for (const filePath of filePaths) if ((await stat(filePath)).size > MAX_CHECK_FILE_BYTES) skippedFiles.push({
496
+ filePath,
497
+ reason: `larger than ${MAX_CHECK_FILE_BYTES} bytes`
498
+ });
499
+ else inputs.push({
500
+ filePath,
501
+ content: await readFile(filePath, "utf8")
502
+ });
503
+ return {
504
+ inputs,
505
+ skippedFiles
506
+ };
507
+ }
508
+ async function checkContracts(context, options = {}) {
509
+ const discovery = await discoverContracts(context);
510
+ const report = {
511
+ version: CONTRACT_REPORT_VERSION,
512
+ root: context.root,
513
+ libraries: discovery.libraries.map(describeLibrary)
514
+ };
515
+ const notChecked = (status, note) => ({
516
+ ...report,
517
+ checkers: [],
518
+ classValidation: "none",
519
+ strict: false,
520
+ files: [],
521
+ diagnostics: discovery.warnings,
522
+ coverage: {
523
+ status,
524
+ checkedFiles: [],
525
+ skippedFiles: [],
526
+ description: `${note} No markup was checked; zero diagnostics does not verify any source.`
527
+ },
528
+ summary: {
529
+ checkedFiles: 0,
530
+ filesWithDiagnostics: 0,
531
+ diagnostics: 0,
532
+ errors: 0,
533
+ warnings: 0
534
+ },
535
+ note
536
+ });
537
+ if (!discovery.contracts.length) return notChecked("no-contracts", NO_CONTRACTS_NOTE);
538
+ const checking = discovery.libraries.filter((library) => library.checkPath);
539
+ if (!checking.length) return notChecked("no-checker", "Contracts were found, but no library declares a checker in \"masterCSS.check\". Read contracts with mastercss_contract_list and mastercss_contract_get.");
540
+ const groups = checking.map((library, index) => ({
541
+ library,
542
+ contracts: discovery.contracts.filter((item) => checking.some((other) => other.name === item.library) ? item.library === library.name : index === 0).map((item) => item.contract)
543
+ })).filter((group) => group.contracts.length);
544
+ const { inputs, skippedFiles } = await readCheckInputs(context, options);
545
+ const manifest = await loadWorkspaceManifest(context);
546
+ const session = manifest.status === "loaded" ? createMCPToolingSession(manifest.manifest, manifest.nativeClassNames) : void 0;
547
+ const checkers = [];
548
+ try {
549
+ let knows;
550
+ if (session) {
551
+ const native = new Set(manifest.nativeClassNames);
552
+ const known = /* @__PURE__ */ new Map();
553
+ knows = (className) => {
554
+ if (native.has(className)) return true;
555
+ let result = known.get(className);
556
+ if (result === void 0) {
557
+ try {
558
+ result = session.inspectClassName(className).matchStatus === "matched";
559
+ } catch {
560
+ result = false;
561
+ }
562
+ known.set(className, result);
563
+ }
564
+ return result;
565
+ };
566
+ }
567
+ const strict = Boolean(knows);
568
+ for (const group of groups) checkers.push({
569
+ ...group,
570
+ ...await startChecker(group.library, group.contracts, knows)
571
+ });
572
+ const results = [];
573
+ for (const input of inputs) {
574
+ const diagnostics = [];
575
+ for (const { library, check } of checkers) {
576
+ const reported = await check(input.content, strict, input.filePath);
577
+ diagnostics.push(...reported.flatMap((diagnostic) => normalizeDiagnostic(diagnostic, library.name)));
578
+ }
579
+ diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
580
+ results.push({
581
+ filePath: input.filePath,
582
+ diagnostics
583
+ });
584
+ }
585
+ const files = results.filter((file) => file.diagnostics.length);
586
+ const flat = files.flatMap((file) => file.diagnostics.map((diagnostic) => ({
587
+ ...diagnostic,
588
+ phase: "contract-check",
589
+ filePath: file.filePath
590
+ })));
591
+ return {
592
+ ...report,
593
+ manifest: manifestMetadata(manifest),
594
+ checkers: checkers.map(({ library, contracts }) => ({
595
+ package: library.name,
596
+ version: library.version,
597
+ contracts: contracts.length
598
+ })),
599
+ classValidation: knows ? "project-manifest" : "none",
600
+ strict,
601
+ files,
602
+ diagnostics: [...discovery.warnings, ...flat],
603
+ coverage: {
604
+ status: inputs.length ? "complete" : "no-inputs",
605
+ checkedFiles: inputs.map((input) => input.filePath),
606
+ skippedFiles,
607
+ description: inputs.length ? "Every path in checkedFiles was read by the checkers listed in checkers. The files array lists only files with diagnostics. A checker reads markup as written: classes built at run time, parts whose root lives in another file, and browser behavior are not checked." : "No markup inputs matched, so no checker ran. Zero diagnostics does not verify any source."
608
+ },
609
+ summary: {
610
+ checkedFiles: inputs.length,
611
+ filesWithDiagnostics: files.length,
612
+ diagnostics: flat.length,
613
+ errors: flat.filter((diagnostic) => diagnostic.severity === "error").length,
614
+ warnings: flat.filter((diagnostic) => diagnostic.severity === "warning").length
615
+ },
616
+ ...knows ? {} : { note: `The project manifest could not be loaded (${manifest.status === "error" ? manifest.error : "unknown"}), so the checker cannot tell a utility from a misspelled class and ran without strict mode.` }
617
+ };
618
+ } finally {
619
+ await Promise.all(checkers.map((checker) => checker.dispose()));
620
+ session?.dispose();
621
+ }
622
+ }
623
+ export { MAX_CHECK_CONTENT_LENGTH, checkContracts, contractCheckerLimits, discoverContracts, getContract, listContracts };
package/dist/format.js CHANGED
@@ -52,7 +52,7 @@ async function previewDirectiveFormat(context, options = {}) {
52
52
  }
53
53
  };
54
54
  }
55
- const files = await resolveSourceFiles(context, options.patterns ?? DEFAULT_DIRECTIVE_FORMAT_PATTERNS);
55
+ const files = await resolveSourceFiles(context, options.patterns, void 0, DEFAULT_DIRECTIVE_FORMAT_PATTERNS);
56
56
  const formattedFiles = await Promise.all(files.map(async (filePath) => {
57
57
  const content = await readFile(filePath, "utf8");
58
58
  return formatContent(service, filePath, content, options.range);
package/dist/lint.js CHANGED
@@ -4,7 +4,7 @@ import { readFile } from "node:fs/promises";
4
4
  import { createToolingSessionSync } from "@master/css-tooling/node";
5
5
  import { fixMasterCSSContent, lintMasterCSSContent, resolveMasterCSSLintRules, summarizeMasterCSSLintFiles } from "@master/css-tooling/lint";
6
6
  // src/lint.ts
7
- const DEFAULT_LINT_SOURCE_PATTERNS = ["**/*.{html,htm,js,jsx,cjs,ts,tsx,mts,cts,svelte,astro,vue,md,mdx,pug,php,css,scss,less}"];
7
+ const DEFAULT_LINT_SOURCE_PATTERNS = ["**/*.{html,htm,js,mjs,jsx,cjs,ts,tsx,mts,cts,svelte,astro,vue,md,mdx,pug,php,css,scss,less}"];
8
8
  const DEFAULT_IGNORE_PATTERNS = ["**/node_modules/**", "node_modules"];
9
9
  const LINT_REPORT_VERSION = 5;
10
10
  async function loadLintState(context, options = {}) {
@@ -14,7 +14,7 @@ async function loadLintState(context, options = {}) {
14
14
  manifest: requireWorkspaceManifest(manifest),
15
15
  nativeClassNames: manifest.nativeClassNames
16
16
  });
17
- const files = await resolveSourceFiles(context, options.patterns ?? DEFAULT_LINT_SOURCE_PATTERNS, options.patterns ? [] : DEFAULT_IGNORE_PATTERNS);
17
+ const files = await resolveSourceFiles(context, options.patterns, options.patterns ? [] : DEFAULT_IGNORE_PATTERNS, DEFAULT_LINT_SOURCE_PATTERNS);
18
18
  return {
19
19
  rules,
20
20
  manifest,
@@ -2,6 +2,7 @@ import { change, classDiff, completion, diagnostic, envelopeMetadata, inspection
2
2
  import { extractSummary, extractedFile, formatFile, formatSummary, lintCoverage, lintFile, lintSummary, previewSummary, scanSummary, scannedFile, scanner, stylesheets } from "./source-schema.js";
3
3
  import { projectComparison, projectSnapshot } from "./project-comparison-schema.js";
4
4
  import { designSystemReport } from "./design-system-schema.js";
5
+ import { contractCheck, contractGet, contractList } from "./contract-schema.js";
5
6
  import * as z from "zod/v4";
6
7
  // src/output-schema.ts
7
8
  const object = z.record(z.string(), z.json());
@@ -178,7 +179,10 @@ const fields = {
178
179
  manifest: metadata,
179
180
  completions: z.array(completion),
180
181
  total: z.number()
181
- }
182
+ },
183
+ mastercss_contract_list: contractList,
184
+ mastercss_contract_get: contractGet,
185
+ mastercss_contract_check: contractCheck
182
186
  };
183
187
  function toolOutputSchema(name) {
184
188
  if (!fields[name]) throw new Error(`Missing output schema for ${name}`);
@@ -8,6 +8,19 @@ const NAME = /^(\d+)-[a-f0-9-]+\.json$/;
8
8
  function missing(error) {
9
9
  return error?.code === "ENOENT";
10
10
  }
11
+ const SHARING = /* @__PURE__ */ new Set([
12
+ "EPERM",
13
+ "EACCES",
14
+ "EBUSY"
15
+ ]);
16
+ async function retrySharing(operation, deadline) {
17
+ while (true) try {
18
+ return await operation();
19
+ } catch (error) {
20
+ if (!SHARING.has(error?.code ?? "") || performance.now() >= deadline) throw error;
21
+ await setTimeout(5);
22
+ }
23
+ }
11
24
  async function remove(file) {
12
25
  try {
13
26
  await unlink(file);
@@ -23,7 +36,7 @@ function alive(pid) {
23
36
  return error.code !== "ESRCH";
24
37
  }
25
38
  }
26
- async function tickets(directory) {
39
+ async function tickets(directory, deadline) {
27
40
  const entries = [];
28
41
  for (const id of await readdir(directory)) {
29
42
  const register = id.endsWith(".next") ? id.slice(0, -5) : id;
@@ -38,7 +51,7 @@ async function tickets(directory) {
38
51
  if (id !== register) continue;
39
52
  let text;
40
53
  try {
41
- text = await readFile(file, "utf8");
54
+ text = await retrySharing(() => readFile(file, "utf8"), deadline);
42
55
  } catch (error) {
43
56
  if (missing(error)) continue;
44
57
  throw error;
@@ -65,21 +78,21 @@ async function withPreviewWriteLock(work, options = {}) {
65
78
  });
66
79
  const publish = async (value) => {
67
80
  await writeFile(file + ".next", JSON.stringify(value), { mode: 384 });
68
- await rename(file + ".next", file);
81
+ await retrySharing(() => rename(file + ".next", file), deadline);
69
82
  };
70
83
  try {
71
84
  await publish({
72
85
  choosing: true,
73
86
  number: "0"
74
87
  });
75
- const number = (await tickets(directory)).reduce((max, peer) => peer.number > max ? peer.number : max, 0n) + 1n;
88
+ const number = (await tickets(directory, deadline)).reduce((max, peer) => peer.number > max ? peer.number : max, 0n) + 1n;
76
89
  await publish({
77
90
  choosing: false,
78
91
  number: String(number)
79
92
  });
80
93
  while (true) {
81
94
  if (performance.now() >= deadline) throw new Error("Timed out waiting for another MCP preview apply.");
82
- if (!(await tickets(directory)).some((peer) => peer.id !== id && (peer.choosing || peer.number < number || peer.number === number && peer.id < id))) return await work();
95
+ if (!(await tickets(directory, deadline)).some((peer) => peer.id !== id && (peer.choosing || peer.number < number || peer.number === number && peer.id < id))) return await work();
83
96
  await setTimeout(10);
84
97
  }
85
98
  } finally {
package/dist/scan.js CHANGED
@@ -9,7 +9,13 @@ import { captureMasterCSSProject, createMasterCSSInspectionReport } from "@maste
9
9
  // src/scan.ts
10
10
  const DEFAULT_SOURCE_PATTERNS = ["**/*.{html,htm,js,jsx,mjs,cjs,ts,tsx,mts,cts,svelte,astro,vue,md,mdx,pug,php}"];
11
11
  const DEFAULT_IGNORE_PATTERNS = ["**/node_modules/**", "node_modules"];
12
- async function resolveSourceFiles(context, patterns = DEFAULT_SOURCE_PATTERNS, ignore = DEFAULT_IGNORE_PATTERNS) {
12
+ /**
13
+ * Resolve source files for a tool. Default patterns follow the source policy, which
14
+ * skips ignored build output such as `dist/`; only patterns the caller was given
15
+ * select ignored files explicitly.
16
+ */
17
+ async function resolveSourceFiles(context, explicitPatterns, ignore = DEFAULT_IGNORE_PATTERNS, defaultPatterns = DEFAULT_SOURCE_PATTERNS) {
18
+ const patterns = [...explicitPatterns ?? defaultPatterns];
13
19
  context.validateGlobPatterns(patterns);
14
20
  const sources = await fg(patterns, {
15
21
  cwd: context.root,
@@ -18,7 +24,7 @@ async function resolveSourceFiles(context, patterns = DEFAULT_SOURCE_PATTERNS, i
18
24
  followSymbolicLinks: false
19
25
  });
20
26
  const policy = new SourcePolicy(context.root, { exclude: ignore });
21
- return Promise.all(sources.filter((source) => policy.accepts(source, patterns !== DEFAULT_SOURCE_PATTERNS)).map((source) => context.resolveExistingFile(source)));
27
+ return Promise.all(sources.filter((source) => policy.accepts(source, explicitPatterns !== void 0)).map((source) => context.resolveExistingFile(source)));
22
28
  }
23
29
  async function scanProject(context, options = {}) {
24
30
  if (options.designSystem && options.context === "preset") throw new Error("designSystem inspection requires project context; omit context or select \"project\".");
package/dist/server.js CHANGED
@@ -14,6 +14,7 @@ import { projectSnapshot } from "./project-comparison-schema.js";
14
14
  import { designSystemOptions } from "./design-system-schema.js";
15
15
  import { toolOutputSchema } from "./output-schema.js";
16
16
  import { suggestClasses } from "./suggest.js";
17
+ import { MAX_CHECK_CONTENT_LENGTH, checkContracts, getContract, listContracts } from "./contracts.js";
17
18
  import { readFileSync } from "node:fs";
18
19
  import { dirname, resolve } from "node:path";
19
20
  import { fileURLToPath } from "node:url";
@@ -372,6 +373,50 @@ function registerTools(server, context) {
372
373
  openWorldHint: false
373
374
  }
374
375
  }, async (input) => executeTool(() => suggestSyntax(context, input)));
376
+ server.registerTool("mastercss_contract_list", {
377
+ title: "List Design System Contracts",
378
+ description: "List the component contracts that the workspace's design-system dependencies declare in \"masterCSS.contracts\", merged with the project's own. Filter by query, tier, or category. Returns an empty list with a note when nothing declares contracts.",
379
+ inputSchema: {
380
+ query: z.string().optional(),
381
+ tier: z.string().optional(),
382
+ category: z.string().optional()
383
+ },
384
+ outputSchema: toolOutputSchema("mastercss_contract_list"),
385
+ annotations: {
386
+ readOnlyHint: true,
387
+ destructiveHint: false,
388
+ idempotentHint: true,
389
+ openWorldHint: false
390
+ }
391
+ }, async (input) => executeTool(() => listContracts(context, input)));
392
+ server.registerTool("mastercss_contract_get", {
393
+ title: "Get Design System Contract",
394
+ description: "Return one full contract and its source by contract name or by any class name the contract defines. Read it before writing that component's markup.",
395
+ inputSchema: { name: z.string().min(1) },
396
+ outputSchema: toolOutputSchema("mastercss_contract_get"),
397
+ annotations: {
398
+ readOnlyHint: true,
399
+ destructiveHint: false,
400
+ idempotentHint: true,
401
+ openWorldHint: false
402
+ }
403
+ }, async (input) => executeTool(() => getContract(context, input)));
404
+ server.registerTool("mastercss_contract_check", {
405
+ title: "Check Markup Against Contracts",
406
+ description: "Check an in-memory source buffer or project files against the merged contracts with the checker module a design-system dependency, or the workspace package itself, declares in \"masterCSS.check\". Runs that package's code in a worker thread with a time and memory limit. Does not write files; reports when no library provides a checker.",
407
+ inputSchema: {
408
+ content: z.string().max(MAX_CHECK_CONTENT_LENGTH).optional(),
409
+ filePath: z.string().optional(),
410
+ patterns: z.array(z.string()).optional()
411
+ },
412
+ outputSchema: toolOutputSchema("mastercss_contract_check"),
413
+ annotations: {
414
+ readOnlyHint: true,
415
+ destructiveHint: false,
416
+ idempotentHint: true,
417
+ openWorldHint: false
418
+ }
419
+ }, async (input) => executeTool(() => checkContracts(context, input)));
375
420
  server.registerTool("mastercss_preview_fixes", {
376
421
  title: "Preview Master CSS Fixes",
377
422
  description: "Create a diff preview for lint fixes or generated CSS output and return a confirmation token.",
package/package.json CHANGED
@@ -50,17 +50,17 @@
50
50
  "provenance": true
51
51
  },
52
52
  "dependencies": {
53
- "@master/css": "2.1.1",
54
- "@master/css-compiler": "2.1.1",
55
- "@master/css-language-service": "2.1.1",
56
- "@master/css-tooling": "2.1.1",
57
- "@master/css-preset": "2.1.1",
58
- "@master/css-schema": "2.1.1",
53
+ "@master/css": "2.1.3",
54
+ "@master/css-compiler": "2.1.3",
55
+ "@master/css-language-service": "2.1.3",
56
+ "@master/css-tooling": "2.1.3",
57
+ "@master/css-preset": "2.1.3",
58
+ "@master/css-schema": "2.1.3",
59
59
  "@modelcontextprotocol/sdk": "^1.32.2",
60
60
  "fast-glob": "^3.3.3",
61
61
  "vscode-languageserver-textdocument": "^1.0.16",
62
62
  "zod": "^4.6.7"
63
63
  },
64
- "version": "2.1.1",
65
- "gitHead": "5a42a239e242afd2722baf1d9dc82ba68114a898"
64
+ "version": "2.1.3",
65
+ "gitHead": "8eb81a6a72e733c43c799659e6493f3f464b8cf9"
66
66
  }