@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 +45 -37
- package/dist/client.js +7 -5
- package/dist/index.js +123 -124
- package/dist/install.js +180 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,62 +1,70 @@
|
|
|
1
1
|
# @sketchxflow/mcp
|
|
2
2
|
|
|
3
|
-
Design websites and
|
|
4
|
-
|
|
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
|
-
##
|
|
7
|
+
## Install (one command, all your agents)
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
2. **Add the server to your MCP client config:**
|
|
9
|
+
```bash
|
|
10
|
+
npx @sketchxflow/mcp install
|
|
11
|
+
```
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `sketchxflow_get_project` | Status
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
59
|
-
node dist/index.js #
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
60
|
+
throw new Error("Timed out after 15 minutes.");
|
|
59
61
|
}
|
|
60
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
85
|
-
// ──
|
|
86
|
-
server.registerTool("
|
|
87
|
-
title: "
|
|
88
|
-
description: "
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
91
|
+
}, async (args) => {
|
|
97
92
|
if (!hasKey())
|
|
98
|
-
return
|
|
93
|
+
return noKey();
|
|
99
94
|
try {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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(`
|
|
106
|
+
return errorText(`Clarify failed: ${e.message}`);
|
|
114
107
|
}
|
|
115
108
|
});
|
|
116
|
-
// ──
|
|
117
|
-
server.registerTool("
|
|
118
|
-
title: "
|
|
119
|
-
description: "
|
|
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("
|
|
122
|
-
|
|
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
|
|
123
|
+
return noKey();
|
|
127
124
|
try {
|
|
128
|
-
const
|
|
129
|
-
|
|
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(`
|
|
142
|
+
return errorText(`Design failed: ${e.message}`);
|
|
133
143
|
}
|
|
134
144
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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()
|
|
140
|
-
|
|
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
|
|
156
|
+
return noKey();
|
|
145
157
|
try {
|
|
146
|
-
const
|
|
147
|
-
|
|
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(`
|
|
165
|
+
return errorText(`Edit failed: ${e.message}`);
|
|
151
166
|
}
|
|
152
167
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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()
|
|
173
|
+
project_id: z.string(),
|
|
174
|
+
slug: z.string().describe("Page slug, e.g. 'home'."),
|
|
158
175
|
},
|
|
159
|
-
}, async (args
|
|
176
|
+
}, async (args) => {
|
|
160
177
|
if (!hasKey())
|
|
161
|
-
return
|
|
178
|
+
return noKey();
|
|
162
179
|
try {
|
|
163
|
-
const
|
|
164
|
-
|
|
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(`
|
|
184
|
+
return errorText(`Get code failed: ${e.message}`);
|
|
171
185
|
}
|
|
172
186
|
});
|
|
173
|
-
// ──
|
|
187
|
+
// ── read ──────────────────────────────────────────────────────────────────────
|
|
174
188
|
server.registerTool("sketchxflow_get_project", {
|
|
175
189
|
title: "Get a project's status",
|
|
176
|
-
description: "
|
|
177
|
-
inputSchema: { project_id: z.string()
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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");
|
package/dist/install.js
ADDED
|
@@ -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.
|
|
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
|
}
|