@sketchxflow/mcp 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,62 +1,70 @@
1
1
  # @sketchxflow/mcp
2
2
 
3
- Design websites and mobile apps with [SketchxFlow](https://sketchxflow.com) from
4
- Claude, Cursor, or any MCP client by prompt, or by planning the structure first.
3
+ Design, extend, edit, and **implement** websites and apps with
4
+ [SketchxFlow](https://sketchxflow.com) directly from Claude Code, Codex, Cursor,
5
+ Claude Desktop, Windsurf, or any MCP client.
5
6
 
6
- ## Setup
7
+ ## Install (one command, all your agents)
7
8
 
8
- 1. **Get an API key:** sign in at [sketchxflow.com](https://sketchxflow.com) →
9
- **Account API keys** → *Generate key*. Copy it (shown once).
10
-
11
- 2. **Add the server to your MCP client config:**
9
+ ```bash
10
+ npx @sketchxflow/mcp install
11
+ ```
12
12
 
13
- ```json
14
- {
15
- "mcpServers": {
16
- "sketchxflow": {
17
- "command": "npx",
18
- "args": ["-y", "@sketchxflow/mcp"],
19
- "env": { "SKETCHXFLOW_API_KEY": "sk_live_…" }
20
- }
21
- }
22
- }
23
- ```
13
+ An interactive wizard detects your installed MCP clients (Claude Desktop, Claude
14
+ Code, Cursor, VS Code, Windsurf, Codex), lets you pick which to set up, asks for
15
+ your API key once, and writes each client's config. Restart the clients it set up
16
+ and you're done.
24
17
 
25
- - **Claude Desktop:** `claude_desktop_config.json`
26
- - **Claude Code:** `claude mcp add sketchxflow -e SKETCHXFLOW_API_KEY=sk_live_… -- npx -y @sketchxflow/mcp`
27
- - **Cursor:** `.cursor/mcp.json`
18
+ > **API key:** sign in at [sketchxflow.com](https://sketchxflow.com) →
19
+ > **Account API keys** *Generate key*.
28
20
 
29
- 3. Restart the client. Ask it to *"design a landing page for a coffee subscription startup"* and watch it build.
21
+ ### Manual config (any client)
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "sketchxflow": {
26
+ "command": "npx",
27
+ "args": ["-y", "@sketchxflow/mcp"],
28
+ "env": { "SKETCHXFLOW_API_KEY": "sk_live_…" }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+ (VS Code uses a `"servers"` key; Codex uses `[mcp_servers.sketchxflow]` in `~/.codex/config.toml` — the wizard handles all of these.)
30
34
 
31
35
  ## Tools
32
36
 
33
37
  | Tool | What it does |
34
38
  |------|--------------|
35
- | `sketchxflow_design` | One-shot: prompt full multi-page design preview URL. |
36
- | `sketchxflow_plan` | Create a site **plan** (pages + purpose) to review before building. |
37
- | `sketchxflow_plan_refine` | Adjust a plan ("add a pricing page", "merge About and Team"). |
38
- | `sketchxflow_generate_from_plan` | Build the approved plan into a design. |
39
- | `sketchxflow_get_project` | Status + preview URL + page list for a project. |
40
- | `sketchxflow_list_projects` | List your projects. |
41
-
42
- Designs are created in **your** SketchxFlow account they appear in your
43
- dashboard, charge your own coin balance, and you can edit, export, or publish
44
- them from the web app. Generation takes ~1–9 minutes; the design/build tools
45
- stream progress and return when ready.
39
+ | `sketchxflow_clarify` | Interview the user (free) to build the brief before designing — returns the next question + a ready-to-use `design_prompt`. |
40
+ | `sketchxflow_design_page` | Generate one page. Omit `project_id` new project; pass one → add a brand-consistent page to it. |
41
+ | `sketchxflow_edit` | Natural-language change to one page section/component (brand-locked). |
42
+ | `sketchxflow_get_code` | Return a page's full HTML (Tailwind inline) so the agent can implement/adapt it in your stack. |
43
+ | `sketchxflow_get_project` / `sketchxflow_list_projects` | Status / list. |
44
+
45
+ ### How a coding agent uses it
46
+ 1. *(optional)* `sketchxflow_clarify` ask the user what they want, one question at a time.
47
+ 2. `sketchxflow_design_page` create the project + first page (or add a page).
48
+ 3. `sketchxflow_get_code` — pull the HTML and re-implement it in the user's framework.
49
+ 4. `sketchxflow_edit` tweak a section; `get_code` again.
50
+
51
+ Each project maps to one SketchxFlow project (visible in your dashboard, charged
52
+ to your coin balance, editable/exportable/publishable from the web app). Design
53
+ and edit run as background jobs; the tools stream progress and return when ready.
46
54
 
47
55
  ## Configuration
48
56
 
49
57
  | Env var | Required | Default | Notes |
50
58
  |---------|----------|---------|-------|
51
59
  | `SKETCHXFLOW_API_KEY` | yes | — | Your `sk_live_…` key. |
52
- | `SKETCHXFLOW_API_URL` | no | `https://sketchxflow.com` | Override the API base (e.g. self-host / staging). |
60
+ | `SKETCHXFLOW_API_URL` | no | `https://sketchxflow.com` | Override the API base. |
53
61
 
54
62
  ## Develop
55
63
 
56
64
  ```bash
57
- npm install
58
- npm run build # tsc dist/
59
- node dist/index.js # runs over stdio (expects an MCP client on stdin/stdout)
65
+ npm install && npm run build
66
+ node dist/index.js # MCP server (stdio)
67
+ node dist/index.js install # setup wizard
60
68
  ```
61
69
 
62
70
  MIT licensed.
package/dist/client.js CHANGED
@@ -47,12 +47,14 @@ async function req(path, method, body) {
47
47
  }
48
48
  return data;
49
49
  }
50
+ const enc = encodeURIComponent;
50
51
  export const sxf = {
51
52
  design: (b) => req("/api/mcp/design", "POST", b),
52
- plan: (b) => req("/api/mcp/plan", "POST", b),
53
- planRefine: (id, instruction) => req(`/api/mcp/plan/${encodeURIComponent(id)}/refine`, "POST", { instruction }),
54
- planGenerate: (id) => req(`/api/mcp/plan/${encodeURIComponent(id)}/generate`, "POST"),
55
- job: (id) => req(`/api/mcp/jobs/${encodeURIComponent(id)}`, "GET"),
56
- project: (id) => req(`/api/mcp/projects/${encodeURIComponent(id)}`, "GET"),
53
+ addPage: (id, b) => req(`/api/mcp/projects/${enc(id)}/pages`, "POST", b),
54
+ edit: (id, slug, instruction) => req(`/api/mcp/projects/${enc(id)}/pages/${enc(slug)}/edit`, "POST", { instruction }),
55
+ getCode: (id, slug) => req(`/api/mcp/projects/${enc(id)}/pages/${enc(slug)}/code`, "GET"),
56
+ clarify: (b) => req("/api/mcp/clarify", "POST", b),
57
+ job: (id) => req(`/api/mcp/jobs/${enc(id)}`, "GET"),
58
+ project: (id) => req(`/api/mcp/projects/${enc(id)}`, "GET"),
57
59
  listProjects: () => req("/api/mcp/projects", "GET"),
58
60
  };
package/dist/index.js CHANGED
@@ -1,38 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * SketchxFlow MCP server.
3
+ * SketchxFlow MCP server + installer.
4
4
  *
5
- * Lets an MCP client (Claude Desktop/Code, Cursor, …) design websites and apps
6
- * via SketchxFlow — direct promptdesign AND a plan→refine→build flow. Generation
7
- * runs as a background job on the server; this process polls it to completion and
8
- * emits MCP progress notifications, so one tool call returns the finished preview
9
- * URL.
5
+ * npx -y @sketchxflow/mcp → run the MCP server (stdio)
6
+ * npx -y @sketchxflow/mcp install interactive setup wizard (all clients)
10
7
  *
11
- * Auth: per-user API key in SKETCHXFLOW_API_KEY (Account API keys).
8
+ * As an MCP server it lets coding agents (Claude Code, Codex, Cursor, …) design,
9
+ * extend, edit, AND implement web/app pages via SketchxFlow. Generation runs as a
10
+ * background job server-side; this process polls to completion with progress
11
+ * notifications. Auth: per-user API key in SKETCHXFLOW_API_KEY.
12
12
  */
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
15
  import { z } from "zod";
16
16
  import { sxf, hasKey } from "./client.js";
17
+ // Subcommand dispatch — keep this BEFORE any stdio wiring.
18
+ const argv = process.argv.slice(2);
19
+ if (argv[0] === "install" || argv[0] === "setup" || argv[0] === "init") {
20
+ const { runInstaller } = await import("./install.js");
21
+ await runInstaller();
22
+ process.exit(0);
23
+ }
17
24
  // IMPORTANT: stdout is the JSON-RPC channel — only ever log to stderr.
18
25
  const log = (...a) => console.error("[sketchxflow-mcp]", ...a);
19
26
  const text = (s) => ({ content: [{ type: "text", text: s }] });
20
27
  const errorText = (s) => ({ content: [{ type: "text", text: s }], isError: true });
21
- /** A progress emitter bound to the current request's progressToken (if any). */
28
+ const noKey = () => errorText("SKETCHXFLOW_API_KEY is not set. Generate one at sketchxflow.com → Account → API keys (or run `npx @sketchxflow/mcp install`).");
22
29
  function progressSender(extra) {
23
30
  const token = extra?._meta?.progressToken;
24
31
  return async (progress, total, message) => {
25
32
  if (token === undefined || typeof extra?.sendNotification !== "function")
26
33
  return;
27
34
  try {
28
- await extra.sendNotification({
29
- method: "notifications/progress",
30
- params: { progressToken: token, progress, total: total || undefined, message },
31
- });
32
- }
33
- catch {
34
- /* progress is best-effort */
35
+ await extra.sendNotification({ method: "notifications/progress", params: { progressToken: token, progress, total: total || undefined, message } });
35
36
  }
37
+ catch { /* best-effort */ }
36
38
  };
37
39
  }
38
40
  const POLL_MS = 4000;
@@ -55,169 +57,166 @@ async function pollJob(jobId, onProgress, signal) {
55
57
  throw new Error(job.error || "Generation failed.");
56
58
  await new Promise((r) => setTimeout(r, POLL_MS));
57
59
  }
58
- throw new Error("Timed out after 15 minutes waiting for the design.");
60
+ throw new Error("Timed out after 15 minutes.");
59
61
  }
60
- function formatResult(result, projectId, previewUrl) {
62
+ /** Format a completed job result. Handles both shapes: a fresh design
63
+ * ({pages:[…]}) and an add/edit ({slug, html}). Always points the agent at
64
+ * get_code to retrieve the HTML to implement. */
65
+ function formatResult(result, projectId, previewUrl, verb) {
61
66
  const pid = result?.project_id || projectId;
62
67
  const url = result?.preview_url || previewUrl;
63
- const pages = (result?.pages || []).map((p) => p.name || p.slug);
64
- return [
65
- `✅ Design ready${result?.title ? `: ${result.title}` : ""}.`,
66
- `Preview: ${url}`,
67
- pages.length ? `Pages (${pages.length}): ${pages.join(", ")}` : "",
68
- `Project ID: ${pid} — open it in the SketchxFlow dashboard to edit, export, or publish.`,
69
- ].filter(Boolean).join("\n");
70
- }
71
- function formatPlan(projectId, plan) {
72
- const brand = plan?.brand_name || plan?.brand?.name || "";
73
- const pages = plan?.pages || [];
74
- const lines = pages.map((p, i) => ` ${i + 1}. ${p.name || p.slug}${p.purpose ? ` — ${p.purpose}` : ""}`);
75
- return [
76
- `📋 Plan created${brand ? ` for ${brand}` : ""}.`,
77
- `Project ID: ${projectId}`,
78
- `Pages (${pages.length}):`,
79
- ...lines,
80
- "",
81
- `Next: refine with sketchxflow_plan_refine (project_id "${projectId}"), or build it now with sketchxflow_generate_from_plan (project_id "${projectId}").`,
82
- ].join("\n");
68
+ const lines = [`✅ ${verb}${result?.title ? `: ${result.title}` : result?.name ? `: ${result.name}` : ""}.`, `Preview: ${url}`, `Project ID: ${pid}`];
69
+ if (Array.isArray(result?.pages) && result.pages.length) {
70
+ lines.push(`Pages: ${result.pages.map((p) => `${p.name} (${p.slug})`).join(", ")}`);
71
+ const first = result.pages[0]?.slug || "home";
72
+ lines.push(`→ Get the code to implement: sketchxflow_get_code(project_id="${pid}", slug="${first}")`);
73
+ }
74
+ else if (result?.slug) {
75
+ lines.push(`Page: ${result.slug}`);
76
+ lines.push(`→ Get the updated code: sketchxflow_get_code(project_id="${pid}", slug="${result.slug}")`);
77
+ }
78
+ return lines.join("\n");
83
79
  }
84
- const server = new McpServer({ name: "sketchxflow", version: "0.1.0" }, { capabilities: { tools: {} } });
85
- // ── Direct design ────────────────────────────────────────────────────────────
86
- server.registerTool("sketchxflow_design", {
87
- title: "Design a website or app",
88
- description: "Generate a complete multi-page website or mobile-app design from a single prompt and return a live preview URL. Use this for a direct, one-shot design. If the user wants to review and shape the page structure FIRST, use sketchxflow_plan instead. Takes 1–9 minutes; this call streams progress and returns when the design is ready.",
80
+ const server = new McpServer({ name: "sketchxflow", version: "0.2.0" }, { capabilities: { tools: {} } });
81
+ // ── clarify: interview the user to build the brief ───────────────────────────
82
+ server.registerTool("sketchxflow_clarify", {
83
+ title: "Ask the user design questions",
84
+ description: "Use BEFORE designing when the brief is thin. Send the user's latest message (and the running `vision` + `history` from prior turns); get back a reply to relay, the updated `vision` board, `ready` (true when there's enough to design), and `missing` topics. When ready, the response includes `design_prompt` pass that to sketchxflow_design_page. Free. Loop this with the user one question at a time.",
89
85
  inputSchema: {
90
- prompt: z.string().describe("The design brief what to build, for whom, and the desired feel."),
91
- platform: z.enum(["web", "mobile"]).optional().describe("Target surface. Default: web."),
92
- ai_images: z.boolean().optional().describe("Generate AI images (default true). false keeps placeholders and saves coins."),
93
- enable_3d: z.boolean().optional().describe("Opt into a real interactive 3D hero (web only). Default false."),
94
- inspiration_url: z.string().optional().describe("A website URL to take visual inspiration from."),
86
+ message: z.string().describe("The user's latest answer / the initial idea."),
87
+ history: z.array(z.object({ role: z.string(), content: z.string() })).optional().describe("Prior turns (echo back what you got last call)."),
88
+ vision: z.any().optional().describe("The `vision` object returned last call pass it back each turn."),
89
+ platform: z.enum(["web", "mobile"]).optional(),
95
90
  },
96
- }, async (args, extra) => {
91
+ }, async (args) => {
97
92
  if (!hasKey())
98
- return errorText("SKETCHXFLOW_API_KEY is not set. Generate a key at sketchxflow.com → Account → API keys and add it to your MCP config.");
93
+ return noKey();
99
94
  try {
100
- const onProgress = progressSender(extra);
101
- await onProgress(0, 0, "Starting design…");
102
- const started = await sxf.design({
103
- prompt: args.prompt,
104
- platform: args.platform,
105
- ai_images: args.ai_images,
106
- enable_3d: args.enable_3d,
107
- inspiration_url: args.inspiration_url,
108
- });
109
- const job = await pollJob(started.job_id, onProgress, extra?.signal);
110
- return text(formatResult(job.result, started.project_id, started.preview_url));
95
+ const r = await sxf.clarify({ message: args.message, history: args.history, vision: args.vision, platform: args.platform });
96
+ const out = [r.reply];
97
+ if (r.missing?.length)
98
+ out.push(`\nStill needed: ${r.missing.join(", ")}`);
99
+ out.push(r.ready ? `\n✅ Ready to design.` : `\n(not ready yet — keep asking)`);
100
+ if (r.design_prompt)
101
+ out.push(`\nWhen the user confirms, design with this prompt:\n"""${r.design_prompt}"""`);
102
+ out.push(`\n[state] pass this back next call → vision: ${JSON.stringify(r.vision)}`);
103
+ return text(out.join("\n"));
111
104
  }
112
105
  catch (e) {
113
- return errorText(`Design failed: ${e.message}`);
106
+ return errorText(`Clarify failed: ${e.message}`);
114
107
  }
115
108
  });
116
- // ── Plan refine build ────────────────────────────────────────────────────
117
- server.registerTool("sketchxflow_plan", {
118
- title: "Plan a site before building",
119
- description: "Create a structured site PLAN (the pages and what each is for) from a brief, WITHOUT building it yet. Show the returned plan to the user; refine it with sketchxflow_plan_refine; then build it with sketchxflow_generate_from_plan. Use this when the user wants to agree on structure before designing.",
109
+ // ── design a page: new project OR add to an existing one ──────────────────────
110
+ server.registerTool("sketchxflow_design_page", {
111
+ title: "Design a page (new project or add to one)",
112
+ description: "Generate ONE polished page. Omit project_id to start a NEW project (creates the project + its first page). Pass an existing project_id to ADD a page to that project (brand-consistent). Returns a preview URL; call sketchxflow_get_code to pull the HTML to implement. Takes 1–9 min; streams progress.",
120
113
  inputSchema: {
121
- prompt: z.string().describe("The brief to plan from."),
122
- platform: z.enum(["web", "mobile"]).optional().describe("Target surface. Default: web."),
114
+ prompt: z.string().describe("What this page is — the brief."),
115
+ project_id: z.string().optional().describe("Existing project to add this page to. Omit to create a new project."),
116
+ name: z.string().optional().describe("Page name/title (used when adding to a project, e.g. 'Pricing'). Defaults from the prompt."),
117
+ platform: z.enum(["web", "mobile"]).optional().describe("Only for a new project. Default web."),
118
+ ai_images: z.boolean().optional(),
119
+ enable_3d: z.boolean().optional(),
123
120
  },
124
- }, async (args) => {
121
+ }, async (args, extra) => {
125
122
  if (!hasKey())
126
- return errorText("SKETCHXFLOW_API_KEY is not set (Account → API keys).");
123
+ return noKey();
127
124
  try {
128
- const r = await sxf.plan({ prompt: args.prompt, platform: args.platform });
129
- return text(formatPlan(r.project_id, r.plan));
125
+ const onProgress = progressSender(extra);
126
+ await onProgress(0, 0, args.project_id ? "Adding page…" : "Starting design…");
127
+ let started;
128
+ let verb;
129
+ if (args.project_id) {
130
+ const name = args.name || args.prompt.split(/[.,\n]/)[0].slice(0, 60) || "New page";
131
+ started = await sxf.addPage(args.project_id, { name, prompt: args.prompt });
132
+ verb = "Page added";
133
+ }
134
+ else {
135
+ started = await sxf.design({ prompt: args.prompt, platform: args.platform, ai_images: args.ai_images, enable_3d: args.enable_3d });
136
+ verb = "Design ready";
137
+ }
138
+ const job = await pollJob(started.job_id, onProgress, extra?.signal);
139
+ return text(formatResult(job.result, started.project_id, started.preview_url, verb));
130
140
  }
131
141
  catch (e) {
132
- return errorText(`Planning failed: ${e.message}`);
142
+ return errorText(`Design failed: ${e.message}`);
133
143
  }
134
144
  });
135
- server.registerTool("sketchxflow_plan_refine", {
136
- title: "Refine a site plan",
137
- description: "Apply a change to an existing plan (created by sketchxflow_plan) and return the updated plan. E.g. 'add a pricing page', 'merge About and Team', 'make it 5 pages max'.",
145
+ // ── edit one section/component of a page ──────────────────────────────────────
146
+ server.registerTool("sketchxflow_edit", {
147
+ title: "Edit a page section/component",
148
+ description: "Apply a natural-language change to one page — e.g. 'make the hero darker', 'tighten the pricing section', 'swap the testimonial for a logo wall'. Brand-locked: edits stay on-palette/on-type. Returns the updated page; call sketchxflow_get_code for the new HTML.",
138
149
  inputSchema: {
139
- project_id: z.string().describe("The project_id returned by sketchxflow_plan."),
140
- instruction: z.string().describe("How to change the plan."),
150
+ project_id: z.string(),
151
+ slug: z.string().describe("Which page (e.g. 'home', 'pricing')."),
152
+ instruction: z.string().describe("The change to make."),
141
153
  },
142
- }, async (args) => {
154
+ }, async (args, extra) => {
143
155
  if (!hasKey())
144
- return errorText("SKETCHXFLOW_API_KEY is not set (Account → API keys).");
156
+ return noKey();
145
157
  try {
146
- const r = await sxf.planRefine(args.project_id, args.instruction);
147
- return text(formatPlan(r.project_id, r.plan));
158
+ const onProgress = progressSender(extra);
159
+ await onProgress(0, 0, "Editing…");
160
+ const started = await sxf.edit(args.project_id, args.slug, args.instruction);
161
+ const job = await pollJob(started.job_id, onProgress, extra?.signal);
162
+ return text(formatResult(job.result, started.project_id, started.preview_url, "Edited"));
148
163
  }
149
164
  catch (e) {
150
- return errorText(`Refine failed: ${e.message}`);
165
+ return errorText(`Edit failed: ${e.message}`);
151
166
  }
152
167
  });
153
- server.registerTool("sketchxflow_generate_from_plan", {
154
- title: "Build the site from its plan",
155
- description: "Build the full design from a plan the user has approved (created via sketchxflow_plan / refined via sketchxflow_plan_refine). Returns a live preview URL. Takes 1–9 minutes; streams progress.",
168
+ // ── get the page HTML (to implement) ──────────────────────────────────────────
169
+ server.registerTool("sketchxflow_get_code", {
170
+ title: "Get a page's HTML to implement",
171
+ description: "Return the full HTML (Tailwind inline) of a generated page so you can re-implement or adapt it into the user's codebase (React/Next/Vue/plain HTML). Call after sketchxflow_design_page / sketchxflow_edit.",
156
172
  inputSchema: {
157
- project_id: z.string().describe("The project_id of the planned project."),
173
+ project_id: z.string(),
174
+ slug: z.string().describe("Page slug, e.g. 'home'."),
158
175
  },
159
- }, async (args, extra) => {
176
+ }, async (args) => {
160
177
  if (!hasKey())
161
- return errorText("SKETCHXFLOW_API_KEY is not set (Account → API keys).");
178
+ return noKey();
162
179
  try {
163
- const onProgress = progressSender(extra);
164
- await onProgress(0, 0, "Starting build…");
165
- const started = await sxf.planGenerate(args.project_id);
166
- const job = await pollJob(started.job_id, onProgress, extra?.signal);
167
- return text(formatResult(job.result, started.project_id, started.preview_url));
180
+ const r = await sxf.getCode(args.project_id, args.slug);
181
+ return text(`Page "${r.slug}" of project ${r.project_id} — preview ${r.preview_url}\n\n\`\`\`html\n${r.html}\n\`\`\``);
168
182
  }
169
183
  catch (e) {
170
- return errorText(`Build failed: ${e.message}`);
184
+ return errorText(`Get code failed: ${e.message}`);
171
185
  }
172
186
  });
173
- // ── Read ─────────────────────────────────────────────────────────────────────
187
+ // ── read ──────────────────────────────────────────────────────────────────────
174
188
  server.registerTool("sketchxflow_get_project", {
175
189
  title: "Get a project's status",
176
- description: "Return a project's status, preview URL, and page list by project_id.",
177
- inputSchema: { project_id: z.string().describe("The project_id.") },
190
+ description: "Project status: preview URL + page list (slug/name).",
191
+ inputSchema: { project_id: z.string() },
178
192
  }, async (args) => {
179
193
  if (!hasKey())
180
- return errorText("SKETCHXFLOW_API_KEY is not set (Account → API keys).");
194
+ return noKey();
181
195
  try {
182
196
  const p = await sxf.project(args.project_id);
183
- const pages = (p.pages || []).map((x) => x.name || x.slug);
184
- return text([
185
- `${p.name || "Project"}${p.title ? ` — ${p.title}` : ""}`,
186
- `Status: ${p.has_html ? "built" : "not built yet"}`,
187
- `Preview: ${p.preview_url}`,
188
- pages.length ? `Pages (${pages.length}): ${pages.join(", ")}` : "",
189
- ].filter(Boolean).join("\n"));
197
+ const pages = (p.pages || []).map((x) => `${x.name} (${x.slug})`);
198
+ return text([`${p.name || "Project"}${p.title ? ` — ${p.title}` : ""}`, `Status: ${p.has_html ? "built" : "not built yet"}`, `Preview: ${p.preview_url}`, pages.length ? `Pages: ${pages.join(", ")}` : ""].filter(Boolean).join("\n"));
190
199
  }
191
200
  catch (e) {
192
201
  return errorText(`Lookup failed: ${e.message}`);
193
202
  }
194
203
  });
195
- server.registerTool("sketchxflow_list_projects", {
196
- title: "List your projects",
197
- description: "List the projects in your SketchxFlow account (most recent first).",
198
- inputSchema: {},
199
- }, async () => {
204
+ server.registerTool("sketchxflow_list_projects", { title: "List your projects", description: "List the projects in your SketchxFlow account (most recent first).", inputSchema: {} }, async () => {
200
205
  if (!hasKey())
201
- return errorText("SKETCHXFLOW_API_KEY is not set (Account → API keys).");
206
+ return noKey();
202
207
  try {
203
208
  const r = await sxf.listProjects();
204
209
  const rows = (r.projects || []).slice(0, 50);
205
210
  if (!rows.length)
206
- return text("No projects yet. Use sketchxflow_design or sketchxflow_plan to create one.");
207
- return text(rows.map((p) => `• ${p.name || p.project_id} — ${p.preview_url}`).join("\n"));
211
+ return text("No projects yet. Use sketchxflow_design_page to create one.");
212
+ return text(rows.map((p) => `• ${p.name || p.project_id} [${p.project_id}] — ${p.preview_url}`).join("\n"));
208
213
  }
209
214
  catch (e) {
210
215
  return errorText(`List failed: ${e.message}`);
211
216
  }
212
217
  });
213
- async function main() {
214
- if (!hasKey())
215
- log("warning: SKETCHXFLOW_API_KEY is not set — tools will return an auth error until it is.");
216
- const transport = new StdioServerTransport();
217
- await server.connect(transport);
218
- log("ready");
219
- }
220
- main().catch((e) => {
221
- log("fatal:", e.message);
222
- process.exit(1);
223
- });
218
+ const transport = new StdioServerTransport();
219
+ await server.connect(transport);
220
+ if (!hasKey())
221
+ log("warning: SKETCHXFLOW_API_KEY is not set — tools will return an auth error until it is.");
222
+ log("ready");
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Interactive setup wizard: `npx @sketchxflow/mcp install`.
3
+ *
4
+ * Detects installed MCP clients, lets the user pick which to configure, asks for
5
+ * the API key once, then writes/merges each client's config in its own format
6
+ * (JSON `mcpServers`, VS Code `servers`, Codex TOML, or the `claude` CLI).
7
+ */
8
+ import prompts from "prompts";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ import { execFileSync } from "node:child_process";
13
+ const PLAT = process.platform;
14
+ // Read live (not at module load) so tests can override HOME.
15
+ function home() { return os.homedir(); }
16
+ function appdata() { return process.env.APPDATA || path.join(home(), "AppData", "Roaming"); }
17
+ function p(...segs) { return path.join(...segs); }
18
+ function appSupport(name) {
19
+ if (PLAT === "darwin")
20
+ return p(home(), "Library", "Application Support", name);
21
+ if (PLAT === "win32")
22
+ return p(appdata(), name);
23
+ return p(home(), ".config", name);
24
+ }
25
+ export function getClients() {
26
+ const HOME = home();
27
+ return [
28
+ {
29
+ id: "claude-desktop", name: "Claude Desktop", kind: "json-mcpServers",
30
+ path: p(appSupport("Claude"), "claude_desktop_config.json"),
31
+ detectPaths: [appSupport("Claude"), "/Applications/Claude.app"],
32
+ restart: "Fully quit and reopen Claude Desktop.",
33
+ },
34
+ {
35
+ id: "claude-code", name: "Claude Code (CLI)", kind: "claude-cli",
36
+ detectPaths: [], // detected via `claude` on PATH below
37
+ restart: "Already active in new sessions (user scope).",
38
+ },
39
+ {
40
+ id: "cursor", name: "Cursor", kind: "json-mcpServers",
41
+ path: p(HOME, ".cursor", "mcp.json"),
42
+ detectPaths: [p(HOME, ".cursor"), "/Applications/Cursor.app"],
43
+ restart: "Restart Cursor (or toggle the server in Settings → MCP).",
44
+ },
45
+ {
46
+ id: "windsurf", name: "Windsurf", kind: "json-mcpServers",
47
+ path: p(HOME, ".codeium", "windsurf", "mcp_config.json"),
48
+ detectPaths: [p(HOME, ".codeium", "windsurf"), "/Applications/Windsurf.app"],
49
+ restart: "Restart Windsurf.",
50
+ },
51
+ {
52
+ id: "vscode", name: "VS Code (Copilot MCP)", kind: "json-servers",
53
+ path: p(appSupport("Code"), "User", "mcp.json"),
54
+ detectPaths: [appSupport("Code"), "/Applications/Visual Studio Code.app"],
55
+ restart: "Reload VS Code (Cmd/Ctrl+Shift+P → Developer: Reload Window).",
56
+ },
57
+ {
58
+ id: "codex", name: "Codex CLI", kind: "toml-codex",
59
+ path: p(HOME, ".codex", "config.toml"),
60
+ detectPaths: [p(HOME, ".codex")],
61
+ restart: "Active on the next `codex` run.",
62
+ },
63
+ ];
64
+ }
65
+ function hasOnPath(bin) {
66
+ try {
67
+ execFileSync(PLAT === "win32" ? "where" : "which", [bin], { stdio: "ignore" });
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ function detected(c) {
75
+ if (c.kind === "claude-cli")
76
+ return hasOnPath("claude");
77
+ return c.detectPaths.some((x) => { try {
78
+ return fs.existsSync(x);
79
+ }
80
+ catch {
81
+ return false;
82
+ } });
83
+ }
84
+ function ensureDir(file) { fs.mkdirSync(path.dirname(file), { recursive: true }); }
85
+ function readJson(file) {
86
+ try {
87
+ return JSON.parse(fs.readFileSync(file, "utf8"));
88
+ }
89
+ catch {
90
+ return {};
91
+ }
92
+ }
93
+ function block(key) {
94
+ return { command: "npx", args: ["-y", "@sketchxflow/mcp"], env: { SKETCHXFLOW_API_KEY: key } };
95
+ }
96
+ function writeJsonClient(c, key) {
97
+ const file = c.path;
98
+ ensureDir(file);
99
+ const cfg = readJson(file);
100
+ const root = c.kind === "json-servers" ? "servers" : "mcpServers";
101
+ cfg[root] = cfg[root] || {};
102
+ cfg[root].sketchxflow = block(key);
103
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
104
+ return file;
105
+ }
106
+ function writeCodex(c, key) {
107
+ const file = c.path;
108
+ ensureDir(file);
109
+ let toml = "";
110
+ try {
111
+ toml = fs.readFileSync(file, "utf8");
112
+ }
113
+ catch { /* new file */ }
114
+ const blockToml = `[mcp_servers.sketchxflow]\n` +
115
+ `command = "npx"\n` +
116
+ `args = ["-y", "@sketchxflow/mcp"]\n` +
117
+ `env = { SKETCHXFLOW_API_KEY = "${key}" }\n`;
118
+ const re = /\[mcp_servers\.sketchxflow\][\s\S]*?(?=\n\[|$)/;
119
+ toml = re.test(toml) ? toml.replace(re, blockToml.trimEnd()) : (toml.trimEnd() + "\n\n" + blockToml);
120
+ fs.writeFileSync(file, toml.replace(/\n{3,}/g, "\n\n"));
121
+ return file;
122
+ }
123
+ function writeClaudeCli(key) {
124
+ try {
125
+ execFileSync("claude", ["mcp", "remove", "sketchxflow", "-s", "user"], { stdio: "ignore" });
126
+ }
127
+ catch { /* not present yet */ }
128
+ execFileSync("claude", ["mcp", "add", "sketchxflow", "-s", "user", "-e", `SKETCHXFLOW_API_KEY=${key}`, "--", "npx", "-y", "@sketchxflow/mcp"], { stdio: "ignore" });
129
+ return "claude (user scope)";
130
+ }
131
+ /** Apply the config for one client. Pure (no prompts) → unit-testable. */
132
+ export function applyToClient(c, key) {
133
+ if (c.kind === "claude-cli")
134
+ return writeClaudeCli(key);
135
+ if (c.kind === "toml-codex")
136
+ return writeCodex(c, key);
137
+ return writeJsonClient(c, key);
138
+ }
139
+ export { detected };
140
+ export async function runInstaller() {
141
+ console.log("\n SketchxFlow MCP — setup\n Design websites & apps from your AI coding agents.\n");
142
+ const CLIENTS = getClients();
143
+ const choices = CLIENTS.map((c) => ({ title: c.name + (detected(c) ? " ✓ detected" : ""), value: c.id, selected: detected(c) }));
144
+ const { picked } = await prompts({
145
+ type: "multiselect", name: "picked", message: "Install into which clients?",
146
+ choices, hint: "space to toggle, enter to confirm", instructions: false,
147
+ });
148
+ if (!picked || !picked.length) {
149
+ console.log("\n Nothing selected — exiting.\n");
150
+ return;
151
+ }
152
+ const { key } = await prompts({
153
+ type: "text", name: "key",
154
+ message: "Your SketchxFlow API key (sk_live_…)",
155
+ initial: process.env.SKETCHXFLOW_API_KEY || "",
156
+ validate: (v) => (v && v.startsWith("sk_")) ? true : "Generate one at sketchxflow.com → Account → API keys",
157
+ });
158
+ if (!key) {
159
+ console.log("\n No key — exiting. (Get one at sketchxflow.com → Account → API keys.)\n");
160
+ return;
161
+ }
162
+ console.log("");
163
+ const results = [];
164
+ for (const id of picked) {
165
+ const c = CLIENTS.find((x) => x.id === id);
166
+ try {
167
+ const where = applyToClient(c, key);
168
+ results.push({ name: c.name, ok: true, where, restart: c.restart });
169
+ console.log(` ✓ ${c.name} → ${where}`);
170
+ }
171
+ catch (e) {
172
+ results.push({ name: c.name, ok: false, where: e.message, restart: c.restart });
173
+ console.log(` ✗ ${c.name} — ${e.message}`);
174
+ }
175
+ }
176
+ console.log("\n Done. To finish, restart the clients you set up:");
177
+ for (const r of results.filter((x) => x.ok))
178
+ console.log(` • ${r.name}: ${r.restart}`);
179
+ console.log("\n Then ask your agent: “design a landing page for …”\n");
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sketchxflow/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "SketchxFlow MCP server — design websites and apps from Claude, Cursor, and any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,10 +29,12 @@
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.29.0",
32
+ "prompts": "^2.4.2",
32
33
  "zod": "^3.25.76"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@types/node": "^26.0.0",
37
+ "@types/prompts": "^2.4.9",
36
38
  "typescript": "^6.0.3"
37
39
  }
38
40
  }