@theme-registry/refract-mcp 0.1.1 → 0.1.2

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
@@ -22,6 +22,7 @@ against it, so the agent asks about *your* theme without ever resending it. It r
22
22
  | `checkContrast` | Do the theme's colour pairings pass WCAG-2 (+ advisory APCA)? |
23
23
  | `validateTheme` | Is a candidate theme valid on every configured target? — returns **every** problem at once (collect-all), per target. |
24
24
  | `diffTheme` | What's the blast radius of a candidate edit — which tokens moved, classes changed, pairings crossed a threshold, targets stopped building? |
25
+ | `reload` | Reload the project's theme config from disk (also happens automatically on change). |
25
26
 
26
27
  `diffTheme` is the plan-then-apply guardrail: pass a **candidate edit** and it builds it against the
27
28
  project's real adapters and reports the blast radius *before* the agent writes — the claim a token file
@@ -31,6 +32,10 @@ a generic check would miss. `getClass` / `resolveToken` read the real emitted na
31
32
  what ships. The query tools take no
32
33
  `theme` argument — they read the loaded project theme.
33
34
 
35
+ Note: over MCP, `getClass` takes **named** arguments — `{ subsystem, group, variant }` (a JSON object,
36
+ as the tool schema requires) — whereas the runtime `theme.getClass(subsystem, group, variant)` takes the
37
+ same three **positionally**. Same identifiers, different call shape for the two surfaces.
38
+
34
39
  ## Architecture
35
40
 
36
41
  Pure tools (`src/tools.ts`, unit-tested) operate on a built `Theme` the server holds; `callTool`
package/dist/server.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, watch } from 'node:fs';
3
- import { dirname } from 'node:path';
2
+ import { readFileSync, watch, realpathSync } from 'node:fs';
3
+ import { basename, dirname } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
5
  import { argv } from 'node:process';
6
- import { pathToFileURL } from 'node:url';
6
+ import { fileURLToPath } from 'node:url';
7
7
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
9
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
@@ -443,9 +443,40 @@ async function start(args) {
443
443
  transport.onclose = () => process.exit(0);
444
444
  await createServer().connect(transport);
445
445
  }
446
- // Run when invoked as the bin entry (ESM: compare our URL to the launched script), not when imported by tests.
447
- if (argv[1] && import.meta.url === pathToFileURL(argv[1]).href) {
448
- void start(argv.slice(2));
446
+ const realpathOr = (p) => {
447
+ try {
448
+ return realpathSync(p);
449
+ }
450
+ catch {
451
+ return p;
452
+ }
453
+ };
454
+ /**
455
+ * Is the launched script (`argv[1]`) this very module, resolving symlinks on BOTH sides? The published
456
+ * bin runs through a `node_modules/.bin/refract-mcp` symlink, so a raw path/URL compare misses (the
457
+ * symlink path ≠ the real dist path) and the server silently exits 0 — a hostile failure mode for
458
+ * something agents auto-spawn. Resolving through realpath fixes it. Pure + injectable `real` so the
459
+ * decision is unit-testable without spawning a process.
460
+ */
461
+ function isBinEntry(invokedAs, selfPath, real = realpathOr) {
462
+ return invokedAs !== undefined && real(invokedAs) === real(selfPath);
463
+ }
464
+ // Auto-start only when invoked as the bin entry. If the launched file IS this file by name but its path
465
+ // won't resolve to this module, say so loudly instead of no-op. A genuine module import (argv[1] is the
466
+ // test runner, a different basename) stays silent.
467
+ {
468
+ const invokedAs = argv[1];
469
+ const selfPath = fileURLToPath(import.meta.url);
470
+ if (isBinEntry(invokedAs, selfPath)) {
471
+ start(argv.slice(2)).catch((e) => {
472
+ process.stderr.write(`refract-mcp: failed to start — ${e.message}\n`);
473
+ process.exit(1);
474
+ });
475
+ }
476
+ else if (invokedAs && basename(invokedAs) === basename(selfPath)) {
477
+ process.stderr.write(`refract-mcp: launched as "${invokedAs}" but that path doesn't resolve to this module (${selfPath}); ` +
478
+ `not auto-starting. Use the \`refract-mcp\` bin or \`node ${selfPath}\`.\n`);
479
+ }
449
480
  }
450
481
 
451
- export { callTool, createServer, guideFiles, loadTheme, serverVersion, start };
482
+ export { callTool, createServer, guideFiles, isBinEntry, loadTheme, serverVersion, start };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theme-registry/refract-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Model Context Protocol server for refract — query + validate a project's theme from an AI agent.",
5
5
  "author": {
6
6
  "name": "Petyo Stoyanov",
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
28
- "@theme-registry/refract": "^0.1.1"
28
+ "@theme-registry/refract": "^0.1.2"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "typescript": "*"
@@ -42,7 +42,7 @@
42
42
  "rollup": "^3.29.4",
43
43
  "typescript": "^5.4.2",
44
44
  "vitest": "^4.1.10",
45
- "@theme-registry/refract-css": "^0.1.1"
45
+ "@theme-registry/refract-css": "^0.1.2"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "rollup -c",