pixl-mcp 0.1.0

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.
Files changed (42) hide show
  1. package/README.md +91 -0
  2. package/dist/design.js +128 -0
  3. package/dist/design.js.map +1 -0
  4. package/dist/flow.js +52 -0
  5. package/dist/flow.js.map +1 -0
  6. package/dist/index.js +34 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/linter/contrast.js +46 -0
  9. package/dist/linter/contrast.js.map +1 -0
  10. package/dist/linter/index.js +190 -0
  11. package/dist/linter/index.js.map +1 -0
  12. package/dist/linter/linter.test.js +99 -0
  13. package/dist/linter/linter.test.js.map +1 -0
  14. package/dist/mcp/tools.js +129 -0
  15. package/dist/mcp/tools.js.map +1 -0
  16. package/dist/presets/catalog.js +162 -0
  17. package/dist/presets/catalog.js.map +1 -0
  18. package/dist/presets/contract.js +140 -0
  19. package/dist/presets/contract.js.map +1 -0
  20. package/dist/presets/types.js +2 -0
  21. package/dist/presets/types.js.map +1 -0
  22. package/dist/preview-server/index.js +143 -0
  23. package/dist/preview-server/index.js.map +1 -0
  24. package/dist/screens.js +52 -0
  25. package/dist/screens.js.map +1 -0
  26. package/dist/session.js +50 -0
  27. package/dist/session.js.map +1 -0
  28. package/dist/workspace/index.js +149 -0
  29. package/dist/workspace/index.js.map +1 -0
  30. package/dist/workspace/schemas.js +48 -0
  31. package/dist/workspace/schemas.js.map +1 -0
  32. package/package.json +47 -0
  33. package/preview-app/index.html +12 -0
  34. package/preview-app/src/App.tsx +53 -0
  35. package/preview-app/src/FlowCanvas.tsx +208 -0
  36. package/preview-app/src/Gallery.tsx +35 -0
  37. package/preview-app/src/api.ts +51 -0
  38. package/preview-app/src/main.tsx +10 -0
  39. package/preview-app/src/styles.css +173 -0
  40. package/preview-app/src/useLiveReload.ts +17 -0
  41. package/preview-app/src/vite-env.d.ts +1 -0
  42. package/preview-app/tsconfig.json +15 -0
@@ -0,0 +1,143 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { createServer as createViteServer } from "vite";
4
+ import react from "@vitejs/plugin-react";
5
+ import { WebSocketServer, WebSocket } from "ws";
6
+ import { FlowSchema } from "../workspace/schemas.js";
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ // dist/preview-server -> package root -> preview-app
9
+ const PREVIEW_APP_ROOT = path.resolve(__dirname, "../../preview-app");
10
+ /**
11
+ * Boots the Vite dev server that hosts the React preview app, wires a WS
12
+ * channel the app subscribes to for live-reload, and exposes a tiny workspace
13
+ * API (screens, flow read/write) the app fetches from.
14
+ */
15
+ export async function startPreviewServer(workspace, port) {
16
+ const vite = await createViteServer({
17
+ root: PREVIEW_APP_ROOT,
18
+ configFile: false,
19
+ plugins: [react()],
20
+ server: {
21
+ port,
22
+ strictPort: false,
23
+ // In Docker, PIXL_HOST=0.0.0.0 makes the forwarded port reachable from the
24
+ // host. Left unset for normal local use so the preview stays on localhost.
25
+ host: process.env.PIXL_HOST || undefined,
26
+ // Allow the forwarded hostname (localhost) through Vite's host check.
27
+ allowedHosts: process.env.PIXL_HOST ? true : undefined,
28
+ },
29
+ clearScreen: false,
30
+ logLevel: "warn",
31
+ });
32
+ attachWorkspaceApi(vite, workspace);
33
+ await vite.listen();
34
+ const address = vite.httpServer.address();
35
+ const resolvedPort = typeof address === "object" && address ? address.port : port;
36
+ const url = `http://localhost:${resolvedPort}`;
37
+ // Reload channel. Use noServer + manual upgrade routing so we only claim
38
+ // /pixl-ws upgrades and leave every other upgrade (crucially Vite's own HMR
39
+ // websocket) untouched. Attaching with { server } would make ws destroy all
40
+ // non-matching upgrade sockets, killing Vite HMR and triggering a reload loop.
41
+ const wss = new WebSocketServer({ noServer: true });
42
+ const httpServer = vite.httpServer;
43
+ httpServer.on("upgrade", (req, socket, head) => {
44
+ let pathname = "";
45
+ try {
46
+ pathname = new URL(req.url ?? "", "http://localhost").pathname;
47
+ }
48
+ catch {
49
+ return;
50
+ }
51
+ if (pathname === "/pixl-ws") {
52
+ wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
53
+ }
54
+ // Any other path (e.g. Vite HMR) is left for Vite's own upgrade handler.
55
+ });
56
+ const broadcast = () => {
57
+ for (const client of wss.clients) {
58
+ if (client.readyState === WebSocket.OPEN)
59
+ client.send(JSON.stringify({ type: "reload" }));
60
+ }
61
+ };
62
+ workspace.on("change", broadcast);
63
+ return {
64
+ url,
65
+ close: async () => {
66
+ workspace.off("change", broadcast);
67
+ wss.close();
68
+ await vite.close();
69
+ },
70
+ };
71
+ }
72
+ /** Read-body helper for the small JSON API. */
73
+ function readBody(req) {
74
+ return new Promise((resolve, reject) => {
75
+ let data = "";
76
+ req.on("data", (c) => (data += c));
77
+ req.on("end", () => resolve(data));
78
+ req.on("error", reject);
79
+ });
80
+ }
81
+ function json(res, status, body) {
82
+ res.writeHead(status, { "Content-Type": "application/json" });
83
+ res.end(JSON.stringify(body));
84
+ }
85
+ /**
86
+ * Connect middleware on the Vite server: everything under /api/* is handled
87
+ * here; anything else falls through to Vite (serving the React app).
88
+ */
89
+ function attachWorkspaceApi(vite, workspace) {
90
+ const handler = async (req, res, next) => {
91
+ const url = req.url ?? "";
92
+ if (!url.startsWith("/api/"))
93
+ return next();
94
+ try {
95
+ // GET /api/status
96
+ if (req.method === "GET" && url === "/api/status") {
97
+ return json(res, 200, await workspace.status(null));
98
+ }
99
+ // GET /api/tokens
100
+ if (req.method === "GET" && url === "/api/tokens") {
101
+ return json(res, 200, await workspace.readTokens());
102
+ }
103
+ // GET /api/screens -> index + inline html for each
104
+ if (req.method === "GET" && url === "/api/screens") {
105
+ const index = await workspace.readScreenIndex();
106
+ const screens = await Promise.all(index.screens.map(async (s) => ({
107
+ ...s,
108
+ html: await readScreenHtml(workspace, s.id),
109
+ })));
110
+ return json(res, 200, { screens });
111
+ }
112
+ // GET /api/flow
113
+ if (req.method === "GET" && url === "/api/flow") {
114
+ return json(res, 200, await workspace.readFlow());
115
+ }
116
+ // POST /api/flow -> persist canvas edits (drag/labels)
117
+ if (req.method === "POST" && url === "/api/flow") {
118
+ const parsed = FlowSchema.parse(JSON.parse(await readBody(req)));
119
+ await workspace.writeFlow(parsed);
120
+ return json(res, 200, { ok: true });
121
+ }
122
+ return json(res, 404, { error: "not found" });
123
+ }
124
+ catch (err) {
125
+ return json(res, 400, { error: err.message });
126
+ }
127
+ };
128
+ // Prepend so /api/* is handled before Vite's SPA/html catch-all middleware.
129
+ vite.middlewares.stack.unshift({
130
+ route: "",
131
+ handle: handler,
132
+ });
133
+ }
134
+ async function readScreenHtml(workspace, id) {
135
+ const { promises: fs } = await import("node:fs");
136
+ try {
137
+ return await fs.readFile(workspace.screenHtmlPath(id), "utf8");
138
+ }
139
+ catch {
140
+ return "";
141
+ }
142
+ }
143
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/preview-server/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import { ScreenSchema } from "./workspace/schemas.js";
2
+ import { lintScreen } from "./linter/index.js";
3
+ function requireTokens(tokens) {
4
+ if (!tokens) {
5
+ throw new Error("No design system set. Call pixl_set_design_system before writing screens.");
6
+ }
7
+ return tokens;
8
+ }
9
+ /**
10
+ * Lint HTML against the active token contract without persisting. Used by
11
+ * pixl_lint_screen and internally by pixl_write_screen.
12
+ */
13
+ export async function lintScreenHtml(workspace, html) {
14
+ const tokens = requireTokens(await workspace.readTokens());
15
+ return lintScreen(html, tokens);
16
+ }
17
+ /**
18
+ * Validate + lint a screen, then persist it only if it passes. On failure the
19
+ * violations are returned so the model can fix and resubmit — the screen is not
20
+ * written, so the gallery never shows off-standard output.
21
+ */
22
+ export async function writeScreen(workspace, input) {
23
+ const tokens = requireTokens(await workspace.readTokens());
24
+ const screen = ScreenSchema.parse({
25
+ id: input.id,
26
+ name: input.name,
27
+ description: input.description,
28
+ });
29
+ const lint = lintScreen(input.html, tokens);
30
+ if (!lint.ok) {
31
+ return {
32
+ ok: false,
33
+ accepted: false,
34
+ screenId: screen.id,
35
+ errorCount: lint.errorCount,
36
+ warnCount: lint.warnCount,
37
+ violations: lint.violations,
38
+ next: "Fix the errors above and resubmit pixl_write_screen. The screen was NOT saved.",
39
+ };
40
+ }
41
+ await workspace.writeScreen(screen, input.html);
42
+ return {
43
+ ok: true,
44
+ accepted: true,
45
+ screenId: screen.id,
46
+ warnCount: lint.warnCount,
47
+ violations: lint.violations, // warnings only at this point
48
+ wrote: [`screens/${screen.id}.html`, "screens.json"],
49
+ next: "Screen saved and rendering in the gallery. Continue with the next screen or set the flow.",
50
+ };
51
+ }
52
+ //# sourceMappingURL=screens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screens.js","sourceRoot":"","sources":["../src/screens.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAe,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,UAAU,EAAmB,MAAM,mBAAmB,CAAC;AAShE,SAAS,aAAa,CAAC,MAAe;IACpC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC/F,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAoB,EAAE,IAAY;IACrE,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,SAAS,CAAC,UAAU,EAAE,CAAC,CAAC;IAC3D,OAAO,UAAU,CAAC,IAAI,EAAE,MAAe,CAAC,CAAC;AAC3C,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,SAAoB,EAAE,KAAuB;IAC7E,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,SAAS,CAAC,UAAU,EAAE,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAW,YAAY,CAAC,KAAK,CAAC;QACxC,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,WAAW,EAAE,KAAK,CAAC,WAAW;KAC/B,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,MAAe,CAAC,CAAC;IACrD,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,IAAI,EAAE,gFAAgF;SACvF,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAChD,OAAO;QACL,EAAE,EAAE,IAAI;QACR,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,MAAM,CAAC,EAAE;QACnB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,8BAA8B;QAC3D,KAAK,EAAE,CAAC,WAAW,MAAM,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC;QACpD,IAAI,EAAE,2FAA2F;KAClG,CAAC;AACJ,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Workspace } from "./workspace/index.js";
3
+ import { startPreviewServer } from "./preview-server/index.js";
4
+ const DEFAULT_PORT = 4321;
5
+ /**
6
+ * Process-wide singleton tying together the workspace and the preview server.
7
+ * The MCP tools operate against this; it stays alive for the life of the
8
+ * `pixl` process so the browser preview persists across tool calls.
9
+ */
10
+ export class PixlSession {
11
+ workspace;
12
+ preview = null;
13
+ constructor(projectRoot = process.cwd()) {
14
+ this.workspace = new Workspace(projectRoot);
15
+ }
16
+ get previewUrl() {
17
+ return this.preview?.url ?? null;
18
+ }
19
+ /** Create the workspace on disk and boot the preview server (idempotent). */
20
+ async ensureStarted(openBrowser) {
21
+ await this.workspace.init();
22
+ this.workspace.startWatching();
23
+ if (!this.preview) {
24
+ this.preview = await startPreviewServer(this.workspace, DEFAULT_PORT);
25
+ if (openBrowser)
26
+ openUrl(this.preview.url);
27
+ }
28
+ return this.preview.url;
29
+ }
30
+ async shutdown() {
31
+ await this.preview?.close();
32
+ this.preview = null;
33
+ await this.workspace.stopWatching();
34
+ }
35
+ }
36
+ function openUrl(url) {
37
+ const cmd = process.platform === "darwin"
38
+ ? "open"
39
+ : process.platform === "win32"
40
+ ? "cmd"
41
+ : "xdg-open";
42
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
43
+ try {
44
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
45
+ }
46
+ catch {
47
+ // best effort — the URL is also returned to the model to relay
48
+ }
49
+ }
50
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAsB,MAAM,2BAA2B,CAAC;AAEnF,MAAM,YAAY,GAAG,IAAI,CAAC;AAE1B;;;;GAIG;AACH,MAAM,OAAO,WAAW;IACb,SAAS,CAAY;IACtB,OAAO,GAAyB,IAAI,CAAC;IAE7C,YAAY,cAAsB,OAAO,CAAC,GAAG,EAAE;QAC7C,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,WAAW,CAAC,CAAC;IAC9C,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC;IACnC,CAAC;IAED,6EAA6E;IAC7E,KAAK,CAAC,aAAa,CAAC,WAAoB;QACtC,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,IAAI,CAAC,OAAO,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;YACtE,IAAI,WAAW;gBAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,MAAM,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,MAAM,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC;IACtC,CAAC;CACF;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,GAAG,GACP,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC3B,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC5B,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,UAAU,CAAC;IACnB,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC7E,IAAI,CAAC;QACH,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;IACjE,CAAC;AACH,CAAC"}
@@ -0,0 +1,149 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { EventEmitter } from "node:events";
4
+ import chokidar from "chokidar";
5
+ import { FlowSchema, ScreenIndexSchema, TokensSchema, } from "./schemas.js";
6
+ /**
7
+ * Owns the .pixl/ directory in the user's project — the single source of truth
8
+ * on disk. Every read/write goes through here so the layout stays consistent,
9
+ * and a chokidar watcher emits "change" so the preview server can live-reload.
10
+ */
11
+ export class Workspace extends EventEmitter {
12
+ root;
13
+ dir;
14
+ watcher = null;
15
+ constructor(projectRoot) {
16
+ super();
17
+ this.root = projectRoot;
18
+ this.dir = path.join(projectRoot, ".pixl");
19
+ }
20
+ // --- paths ---------------------------------------------------------------
21
+ p(...parts) {
22
+ return path.join(this.dir, ...parts);
23
+ }
24
+ get tokensPath() {
25
+ return this.p("tokens.json");
26
+ }
27
+ get flowPath() {
28
+ return this.p("flow.json");
29
+ }
30
+ get screenIndexPath() {
31
+ return this.p("screens.json");
32
+ }
33
+ get designDir() {
34
+ return this.p("design");
35
+ }
36
+ get screensDir() {
37
+ return this.p("screens");
38
+ }
39
+ screenHtmlPath(id) {
40
+ return path.join(this.screensDir, `${id}.html`);
41
+ }
42
+ // --- lifecycle -----------------------------------------------------------
43
+ async init() {
44
+ await fs.mkdir(this.designDir, { recursive: true });
45
+ await fs.mkdir(this.screensDir, { recursive: true });
46
+ if (!(await exists(this.screenIndexPath))) {
47
+ await this.writeScreenIndex({ screens: [] });
48
+ }
49
+ if (!(await exists(this.flowPath))) {
50
+ await this.writeFlow({ nodes: [], edges: [] });
51
+ }
52
+ }
53
+ async isInitialized() {
54
+ return exists(this.dir);
55
+ }
56
+ /** Watch the workspace and emit "change" (debounced) on any file event. */
57
+ startWatching() {
58
+ if (this.watcher)
59
+ return;
60
+ let timer = null;
61
+ const fire = (file) => {
62
+ if (timer)
63
+ clearTimeout(timer);
64
+ timer = setTimeout(() => this.emit("change", file), 80);
65
+ };
66
+ this.watcher = chokidar.watch(this.dir, {
67
+ ignoreInitial: true,
68
+ awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 20 },
69
+ });
70
+ this.watcher.on("all", (_event, file) => fire(file));
71
+ }
72
+ async stopWatching() {
73
+ await this.watcher?.close();
74
+ this.watcher = null;
75
+ }
76
+ // --- tokens --------------------------------------------------------------
77
+ async writeTokens(tokens) {
78
+ const parsed = TokensSchema.parse(tokens);
79
+ await writeJson(this.tokensPath, parsed);
80
+ }
81
+ async readTokens() {
82
+ return readJson(this.tokensPath, TokensSchema);
83
+ }
84
+ // --- design markdown -----------------------------------------------------
85
+ async writeDesignDoc(name, markdown) {
86
+ await fs.mkdir(this.designDir, { recursive: true });
87
+ await fs.writeFile(this.p("design", `${name}.md`), markdown, "utf8");
88
+ }
89
+ // --- screens -------------------------------------------------------------
90
+ async writeScreenIndex(index) {
91
+ await writeJson(this.screenIndexPath, ScreenIndexSchema.parse(index));
92
+ }
93
+ async readScreenIndex() {
94
+ return (await readJson(this.screenIndexPath, ScreenIndexSchema)) ?? { screens: [] };
95
+ }
96
+ async writeScreen(screen, html) {
97
+ await fs.mkdir(this.screensDir, { recursive: true });
98
+ await fs.writeFile(this.screenHtmlPath(screen.id), html, "utf8");
99
+ const index = await this.readScreenIndex();
100
+ const others = index.screens.filter((s) => s.id !== screen.id);
101
+ await this.writeScreenIndex({ screens: [...others, screen] });
102
+ }
103
+ // --- flow ----------------------------------------------------------------
104
+ async writeFlow(flow) {
105
+ await writeJson(this.flowPath, FlowSchema.parse(flow));
106
+ }
107
+ async readFlow() {
108
+ return (await readJson(this.flowPath, FlowSchema)) ?? { nodes: [], edges: [] };
109
+ }
110
+ // --- status --------------------------------------------------------------
111
+ async status(previewUrl) {
112
+ const initialized = await this.isInitialized();
113
+ const tokens = initialized ? await this.readTokens() : null;
114
+ const index = initialized ? await this.readScreenIndex() : { screens: [] };
115
+ const flow = initialized ? await this.readFlow() : { nodes: [], edges: [] };
116
+ return {
117
+ root: this.root,
118
+ initialized,
119
+ designSystem: tokens?.name ?? null,
120
+ screenCount: index.screens.length,
121
+ flowNodeCount: flow.nodes.length,
122
+ previewUrl,
123
+ };
124
+ }
125
+ }
126
+ // --- fs helpers ------------------------------------------------------------
127
+ async function exists(p) {
128
+ try {
129
+ await fs.access(p);
130
+ return true;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
136
+ async function writeJson(p, data) {
137
+ await fs.mkdir(path.dirname(p), { recursive: true });
138
+ await fs.writeFile(p, JSON.stringify(data, null, 2), "utf8");
139
+ }
140
+ async function readJson(p, schema) {
141
+ try {
142
+ const raw = await fs.readFile(p, "utf8");
143
+ return schema.parse(JSON.parse(raw));
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/workspace/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Design tokens — the locked contract every generated screen must obey.
4
+ * A preset (or the custom interview) produces exactly this shape.
5
+ */
6
+ export const TokensSchema = z.object({
7
+ name: z.string(),
8
+ preset: z.string().optional(),
9
+ typeScale: z.array(z.number()).describe("allowed font sizes in px, ascending"),
10
+ fontFamilies: z.array(z.string()).describe("allowed font-family stacks"),
11
+ spacing: z.array(z.number()).describe("allowed spacing/size steps in px"),
12
+ radius: z.array(z.number()).describe("allowed border-radius values in px"),
13
+ colors: z
14
+ .record(z.string())
15
+ .describe("named color roles -> hex, e.g. { bg: '#fff', text: '#111', primary: '#4f46e5' }"),
16
+ shadows: z.array(z.string()).describe("allowed box-shadow values"),
17
+ breakpoints: z
18
+ .object({ width: z.number(), height: z.number() })
19
+ .describe("phone viewport the screens are locked to"),
20
+ });
21
+ /** One generated screen. HTML lives in screens/<id>.html; this is the index entry. */
22
+ export const ScreenSchema = z.object({
23
+ id: z.string().regex(/^[a-z0-9-]+$/, "id must be kebab-case"),
24
+ name: z.string(),
25
+ description: z.string().optional(),
26
+ });
27
+ export const ScreenIndexSchema = z.object({
28
+ screens: z.array(ScreenSchema).default([]),
29
+ });
30
+ /** User-journey flow graph. */
31
+ export const FlowNodeSchema = z.object({
32
+ id: z.string(),
33
+ screenId: z.string(),
34
+ label: z.string(),
35
+ x: z.number(),
36
+ y: z.number(),
37
+ });
38
+ export const FlowEdgeSchema = z.object({
39
+ id: z.string(),
40
+ source: z.string().describe("source node id"),
41
+ target: z.string().describe("target node id"),
42
+ label: z.string().default(""),
43
+ });
44
+ export const FlowSchema = z.object({
45
+ nodes: z.array(FlowNodeSchema).default([]),
46
+ edges: z.array(FlowEdgeSchema).default([]),
47
+ });
48
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/workspace/schemas.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "pixl-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Open-source MCP server that turns any AI coding host into a guided mobile-UI design tool: a design-system token contract, a taste linter that rejects off-standard screens, and a local browser preview with a phone gallery and a draggable user-journey flow map.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "pixl": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "preview-app"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "dev": "tsx src/index.ts",
17
+ "start": "node dist/index.js",
18
+ "typecheck": "tsc -p tsconfig.json --noEmit",
19
+ "test": "vitest run",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.12.0",
24
+ "@vitejs/plugin-react": "^4.7.0",
25
+ "@xyflow/react": "^12.11.2",
26
+ "chokidar": "^4.0.0",
27
+ "dagre": "^0.8.5",
28
+ "react": "^19.2.7",
29
+ "react-dom": "^19.2.7",
30
+ "vite": "^6.0.0",
31
+ "ws": "^8.18.0",
32
+ "zod": "^3.24.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/dagre": "^0.7.54",
36
+ "@types/node": "^22.0.0",
37
+ "@types/react": "^19.2.17",
38
+ "@types/react-dom": "^19.2.3",
39
+ "@types/ws": "^8.18.1",
40
+ "tsx": "^4.19.0",
41
+ "typescript": "^5.7.0",
42
+ "vitest": "^2.1.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=20"
46
+ }
47
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Pixl</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,53 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { api, type Screen, type Status } from "./api";
3
+ import { useLiveReload } from "./useLiveReload";
4
+ import { Gallery } from "./Gallery";
5
+ import { FlowCanvas } from "./FlowCanvas";
6
+
7
+ type Tab = "gallery" | "flow";
8
+
9
+ export function App() {
10
+ const [tab, setTab] = useState<Tab>("gallery");
11
+ const [status, setStatus] = useState<Status | null>(null);
12
+ const [screens, setScreens] = useState<Screen[]>([]);
13
+ const [version, setVersion] = useState(0);
14
+
15
+ const refresh = useCallback(async () => {
16
+ const [s, sc] = await Promise.all([api.status(), api.screens()]);
17
+ setStatus(s);
18
+ setScreens(sc.screens);
19
+ setVersion((v) => v + 1);
20
+ }, []);
21
+
22
+ useEffect(() => {
23
+ refresh();
24
+ }, [refresh]);
25
+ useLiveReload(refresh);
26
+
27
+ return (
28
+ <div className="app">
29
+ <header className="topbar">
30
+ <div className="brand">
31
+ Pixl<span className="dot">.</span>
32
+ </div>
33
+ <nav className="tabs">
34
+ <button className={tab === "gallery" ? "active" : ""} onClick={() => setTab("gallery")}>
35
+ Screens {status ? `(${status.screenCount})` : ""}
36
+ </button>
37
+ <button className={tab === "flow" ? "active" : ""} onClick={() => setTab("flow")}>
38
+ Flow {status ? `(${status.flowNodeCount})` : ""}
39
+ </button>
40
+ </nav>
41
+ <div className="meta">{status?.designSystem ?? "no design system yet"}</div>
42
+ </header>
43
+
44
+ <main className="content">
45
+ {tab === "gallery" ? (
46
+ <Gallery screens={screens} />
47
+ ) : (
48
+ <FlowCanvas screens={screens} version={version} />
49
+ )}
50
+ </main>
51
+ </div>
52
+ );
53
+ }