one-design-mcp 0.2.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/dist/server.js +152 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -103,6 +103,17 @@ the same email (`sign_in`). The session is kept in `~/.config/one-design/mcp-acc
103
103
 
104
104
  Needs Node.js 20 or newer.
105
105
 
106
+ **One command** (adds it to Claude Desktop, Cursor and Claude Code, wherever they're installed, and
107
+ keeps a `.bak` of any config it changes):
108
+
109
+ ```bash
110
+ npx -y one-design-mcp setup
111
+ ```
112
+
113
+ `--dry-run` shows what it would change, `--app desktop|cursor|code` picks one app, and `--remove`
114
+ takes it out again. An agent with a terminal (Claude Code, Cursor) can run it for you: ask it to
115
+ "set up the One Design MCP server with npx -y one-design-mcp setup".
116
+
106
117
  **Claude Code**
107
118
 
108
119
  ```bash
@@ -129,6 +140,8 @@ claude mcp add one-design -- node /path/to/one-design/mcp/dist/server.js
129
140
 
130
141
  The build takes the site's public Supabase address and anon key from `../.env.local` (or
131
142
  `ONE_DESIGN_SUPABASE_URL` / `ONE_DESIGN_SUPABASE_ANON_KEY`) so sign-in and checkout work.
132
- `ONE_DESIGN_PAYMENTS=off` makes a free-beta build with every tool open. Link Lock's encryption is
143
+ `ONE_DESIGN_PAYMENTS=off` makes a free-beta build with every tool open. These are build settings
144
+ only: at run time nothing a user sets changes the plan (no environment variables, and the plan is
145
+ never read back from the saved session; it's checked against the account and kept in memory). Link Lock's encryption is
133
146
  the website's own (`src/lib/linklock.ts`), bundled at build time, so links made here open on
134
147
  one.design. Plan rules mirror `src/lib/plans.ts` and `src/lib/plan.ts`: keep `src/plan.ts` in step.
package/dist/server.js CHANGED
@@ -1,10 +1,122 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
11
+ };
3
12
  var __export = (target, all) => {
4
13
  for (var name in all)
5
14
  __defProp(target, name, { get: all[name], enumerable: true });
6
15
  };
7
16
 
17
+ // src/setup.ts
18
+ var setup_exports = {};
19
+ __export(setup_exports, {
20
+ setup: () => setup
21
+ });
22
+ import { promises as fs9, existsSync } from "node:fs";
23
+ import path9 from "node:path";
24
+ import os7 from "node:os";
25
+ import { execFileSync as execFileSync2 } from "node:child_process";
26
+ function entry() {
27
+ return { command: npxPath(), args: ["-y", "one-design-mcp@latest"] };
28
+ }
29
+ function npxPath() {
30
+ try {
31
+ const found = execFileSync2(process.platform === "win32" ? "where" : "which", ["npx"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split(/\r?\n/)[0].trim();
32
+ if (found) return found;
33
+ } catch {
34
+ }
35
+ const beside = path9.join(path9.dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx");
36
+ return existsSync(beside) ? beside : "npx";
37
+ }
38
+ function desktopConfig() {
39
+ const home = os7.homedir();
40
+ if (process.platform === "darwin") return path9.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
41
+ if (process.platform === "win32") return path9.join(process.env.APPDATA ?? path9.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
42
+ return path9.join(process.env.XDG_CONFIG_HOME ?? path9.join(home, ".config"), "Claude", "claude_desktop_config.json");
43
+ }
44
+ async function editJson(file, remove, dry) {
45
+ if (!existsSync(path9.dirname(file))) return "not installed here, skipped";
46
+ let config = {};
47
+ let before = "";
48
+ if (existsSync(file)) {
49
+ before = await fs9.readFile(file, "utf8");
50
+ try {
51
+ config = before.trim() ? JSON.parse(before) : {};
52
+ } catch {
53
+ return `its config isn\u2019t valid JSON, left alone: ${file}`;
54
+ }
55
+ }
56
+ const servers = { ...config.mcpServers ?? {} };
57
+ const had = NAME in servers;
58
+ if (remove) {
59
+ if (!had) return "not set up there, nothing to remove";
60
+ delete servers[NAME];
61
+ } else servers[NAME] = entry();
62
+ const after = JSON.stringify({ ...config, mcpServers: servers }, null, 2) + "\n";
63
+ if (after === before) return "already set up";
64
+ if (dry) return remove ? "would remove One Design" : had ? "would update One Design" : "would add One Design";
65
+ await fs9.mkdir(path9.dirname(file), { recursive: true });
66
+ if (before) await fs9.writeFile(`${file}.bak`, before);
67
+ await fs9.writeFile(file, after);
68
+ return `${remove ? "removed" : had ? "updated" : "added"} (${file}${before ? ", old copy saved as .bak" : ""})`;
69
+ }
70
+ function claudeCode(remove, dry) {
71
+ try {
72
+ execFileSync2(process.platform === "win32" ? "where" : "which", ["claude"], { stdio: "ignore" });
73
+ } catch {
74
+ return "not installed here, skipped";
75
+ }
76
+ const list = (() => {
77
+ try {
78
+ return execFileSync2("claude", ["mcp", "list"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
79
+ } catch {
80
+ return "";
81
+ }
82
+ })();
83
+ const had = new RegExp(`^${NAME}:`, "m").test(list);
84
+ if (remove) {
85
+ if (!had) return "not set up there, nothing to remove";
86
+ if (dry) return "would remove One Design";
87
+ execFileSync2("claude", ["mcp", "remove", NAME, "--scope", "user"], { stdio: "ignore" });
88
+ return "removed";
89
+ }
90
+ if (had) return "already set up";
91
+ if (dry) return "would add One Design (for every project)";
92
+ execFileSync2("claude", ["mcp", "add", "--scope", "user", NAME, "--", "npx", "-y", "one-design-mcp@latest"], { stdio: "ignore" });
93
+ return "added (for every project)";
94
+ }
95
+ async function setup(argv) {
96
+ const dry = argv.includes("--dry-run");
97
+ const remove = argv.includes("--remove");
98
+ const only = argv[argv.indexOf("--app") + 1];
99
+ const want = (app) => !argv.includes("--app") || only === app;
100
+ const lines = [];
101
+ if (want("desktop")) lines.push(`Claude Desktop: ${await editJson(desktopConfig(), remove, dry)}`);
102
+ if (want("cursor")) lines.push(`Cursor: ${await editJson(cursorConfig(), remove, dry)}`);
103
+ if (want("code")) lines.push(`Claude Code: ${claudeCode(remove, dry)}`);
104
+ console.log(`One Design for Claude${dry ? " (dry run, nothing changed)" : ""}
105
+
106
+ ${lines.map((l) => ` ${l}`).join("\n")}
107
+ `);
108
+ if (!dry && !remove && lines.some((l) => /added|updated/.test(l)))
109
+ console.log("Next: quit and reopen the app (on a Mac, \u2318Q), then ask Claude \u201CWhat can One Design do?\u201D.\nOther MCP apps: add a server with the command npx and the arguments -y one-design-mcp@latest.");
110
+ }
111
+ var NAME, cursorConfig;
112
+ var init_setup = __esm({
113
+ "src/setup.ts"() {
114
+ "use strict";
115
+ NAME = "one-design";
116
+ cursorConfig = () => path9.join(os7.homedir(), ".cursor", "mcp.json");
117
+ }
118
+ });
119
+
8
120
  // src/server.ts
9
121
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
122
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -223,9 +335,9 @@ import { promises as fs2 } from "node:fs";
223
335
  import path2 from "node:path";
224
336
  import os2 from "node:os";
225
337
  import { exec } from "node:child_process";
226
- var SB_URL = process.env.ONE_DESIGN_SUPABASE_URL || (true ? "https://jaznopltkefnwooyksrk.supabase.co" : "");
227
- var SB_KEY = process.env.ONE_DESIGN_SUPABASE_ANON_KEY || (true ? "sb_publishable_WfXjMAOVoXE9dtJMfJCshw_9rfMboU7" : "");
228
- var PAYMENTS = (process.env.ONE_DESIGN_PAYMENTS || (true ? "on" : "on")).toLowerCase();
338
+ var SB_URL = true ? "https://jaznopltkefnwooyksrk.supabase.co" : "";
339
+ var SB_KEY = true ? "sb_publishable_WfXjMAOVoXE9dtJMfJCshw_9rfMboU7" : "";
340
+ var PAYMENTS = (true ? "on" : "on").toLowerCase();
229
341
  var SITE = "https://one.design";
230
342
  var PRICING_URL = `${SITE}/pricing`;
231
343
  var ACCOUNT_URL = `${SITE}/account`;
@@ -276,18 +388,19 @@ function isPaid(s) {
276
388
  if (["active", "on_trial", "past_due"].includes(s.status)) return true;
277
389
  return s.status === "cancelled" && !!s.ends_at && new Date(s.ends_at) > /* @__PURE__ */ new Date();
278
390
  }
391
+ var checked = null;
279
392
  async function currentPlan(force = false) {
280
393
  if (freeBeta()) return { plan: "pro", label: "Free beta: every tool is open to everyone for now" };
281
394
  if (!accountsAvailable()) return { plan: "free", label: "Free (sign-in isn\u2019t set up in this build)" };
282
395
  const s = await freshSession();
283
396
  if (!s) return { plan: "free", label: "Free (not signed in)" };
284
- if (!force && s.plan && s.checked_at && Date.now() - s.checked_at < 10 * 60 * 1e3) return { plan: s.plan, email: s.email, label: s.planLabel ?? s.plan };
397
+ if (!force && checked && checked.user === s.user_id && Date.now() - checked.at < 10 * 60 * 1e3) return { plan: checked.plan, email: s.email, label: checked.label };
285
398
  const r = await sb(`/rest/v1/subscriptions?select=plan,status,ends_at&user_id=eq.${s.user_id}`, { token: s.access_token });
286
399
  const rows = r.ok ? await r.json() : [];
287
400
  const sub = rows[0] ?? null;
288
401
  const paid = isPaid(sub);
289
402
  const label = paid ? sub.plan === "lifetime" ? "Pro (Lifetime)" : "Pro" : "Free";
290
- await writeSession({ ...s, plan: paid ? "pro" : "free", planLabel: label, checked_at: Date.now() });
403
+ if (r.ok) checked = { user: s.user_id, plan: paid ? "pro" : "free", label, at: Date.now() };
291
404
  return { plan: paid ? "pro" : "free", email: s.email, label };
292
405
  }
293
406
  async function sendCode(email) {
@@ -306,6 +419,7 @@ async function verifyCode(email, code) {
306
419
  return currentPlan(true);
307
420
  }
308
421
  async function signOut() {
422
+ checked = null;
309
423
  await writeSession(null);
310
424
  }
311
425
  async function checkoutUrl() {
@@ -324,6 +438,12 @@ async function billingPortalUrl() {
324
438
  const j = await r.json().catch(() => ({}));
325
439
  return j.url ?? ACCOUNT_URL;
326
440
  }
441
+ async function librariesGrandfathered() {
442
+ const s = await freshSession();
443
+ if (!s) return false;
444
+ const r = await sb("/rest/v1/rpc/libraries_grandfathered", { method: "POST", body: "{}", token: s.access_token });
445
+ return r.ok && await r.json().catch(() => false) === true;
446
+ }
327
447
  async function brandLibraries() {
328
448
  const s = await freshSession();
329
449
  if (!s) throw new Error("Sign in first (sign_in), with the email you use on one.design.");
@@ -601,7 +721,9 @@ function register(t2) {
601
721
  });
602
722
  t2.tool({
603
723
  name: "brand_library",
604
- siteId: "brand-library",
724
+ // Not gated by the usual Pro check: accounts that had libraries before they became Pro-only keep
725
+ // them (checked below, and enforced by the database)
726
+ siteId: "",
605
727
  title: "Your Brand Libraries",
606
728
  description: "Reads the Brand Libraries synced to the signed-in one.design account: colours (name and HEX), fonts, logos. Use the colours in other tools (QR codes, watermarks, store shots, bento). save_logos writes the logos to a folder so other tools can use them.",
607
729
  openWorld: true,
@@ -610,7 +732,8 @@ function register(t2) {
610
732
  save_logos: z.boolean().default(false).describe("Save the logos (and fonts) as files."),
611
733
  output_dir: z.string().optional().describe("Where to save them. Default: ~/Downloads/one-design.")
612
734
  },
613
- async run(a) {
735
+ async run(a, ctx) {
736
+ if (ctx.plan !== "pro" && !await librariesGrandfathered()) return text(proPitch("Your Brand Libraries"), true);
614
737
  let libs = await brandLibraries();
615
738
  if (a.library) libs = libs.filter((l) => l.name.toLowerCase() === a.library.toLowerCase());
616
739
  if (!libs.length) throw new ToolError(a.library ? `No Brand Library called ${a.library}.` : "This account has no Brand Libraries yet. Make one at https://one.design/brand.");
@@ -5006,9 +5129,9 @@ async function postage(img, o) {
5006
5129
  const inset = bordered ? margin : Math.round(hole * 2.2);
5007
5130
  const value = o.value.trim();
5008
5131
  if (value) {
5009
- const fs9 = Math.round(unit * 0.09);
5010
- const tw = await measure(value, fs9, 400, "serif");
5011
- const pad = fs9 * 0.35;
5132
+ const fs10 = Math.round(unit * 0.09);
5133
+ const tw = await measure(value, fs10, 400, "serif");
5134
+ const pad = fs10 * 0.35;
5012
5135
  const bx = Math.max(0, Math.round(margin + iw - tw - pad * 2 - (bordered ? 0 : inset)));
5013
5136
  const by = Math.max(0, Math.round(inset));
5014
5137
  const flat = await sharp(pic).flatten({ background: o.paper }).raw().toBuffer({ resolveWithObject: true });
@@ -5017,7 +5140,7 @@ async function postage(img, o) {
5017
5140
  const x0 = Math.max(0, bx - margin);
5018
5141
  const y0 = Math.max(0, by - margin);
5019
5142
  const x1 = Math.min(iw, bx - margin + Math.round(tw + pad * 2));
5020
- const y1 = Math.min(ih, by - margin + Math.round(fs9 + pad * 1.2));
5143
+ const y1 = Math.min(ih, by - margin + Math.round(fs10 + pad * 1.2));
5021
5144
  for (let y = y0; y < y1; y++)
5022
5145
  for (let x = x0; x < x1; x++) {
5023
5146
  const i = (y * iw + x) * flat.info.channels;
@@ -5025,18 +5148,18 @@ async function postage(img, o) {
5025
5148
  cnt++;
5026
5149
  }
5027
5150
  const under = cnt ? sum / cnt : 255;
5028
- body.push(text2(margin + iw - tw - pad - (bordered ? 0 : inset), fromTop(inset + pad * 0.6, fs9), value, { size: fs9, font: "serif", fill: under < 110 ? o.paper : inkOnPaper }));
5151
+ body.push(text2(margin + iw - tw - pad - (bordered ? 0 : inset), fromTop(inset + pad * 0.6, fs10), value, { size: fs10, font: "serif", fill: under < 110 ? o.paper : inkOnPaper }));
5029
5152
  }
5030
5153
  const caption = o.caption.trim();
5031
5154
  if (caption) {
5032
5155
  if (band) {
5033
- const fs9 = await fitSize(caption, W - margin * 2, Math.round(band * 0.6), 400, "serif");
5034
- body.push(text2(W / 2, fromMiddle(margin + ih + band / 2 + margin * 0.3, fs9), caption, { size: fs9, font: "serif", fill: inkOnPaper, anchor: "middle" }));
5156
+ const fs10 = await fitSize(caption, W - margin * 2, Math.round(band * 0.6), 400, "serif");
5157
+ body.push(text2(W / 2, fromMiddle(margin + ih + band / 2 + margin * 0.3, fs10), caption, { size: fs10, font: "serif", fill: inkOnPaper, anchor: "middle" }));
5035
5158
  } else {
5036
5159
  const max = Math.round(unit * 0.085);
5037
- const fs9 = await fitSize(caption, W - inset * 2, max, 400, "serif");
5160
+ const fs10 = await fitSize(caption, W - inset * 2, max, 400, "serif");
5038
5161
  defs.push(`<filter id="capshadow" x="-20%" y="-50%" width="140%" height="200%"><feDropShadow dx="0" dy="0" stdDeviation="${n(max * 0.2)}" flood-color="#000000" flood-opacity="0.6"/></filter>`);
5039
- body.push(text2(W / 2, fromMiddle(H - inset - max * 0.55, fs9), caption, { size: fs9, font: "serif", fill: o.paper, anchor: "middle", extra: 'filter="url(#capshadow)"' }));
5162
+ body.push(text2(W / 2, fromMiddle(H - inset - max * 0.55, fs10), caption, { size: fs10, font: "serif", fill: o.paper, anchor: "middle", extra: 'filter="url(#capshadow)"' }));
5040
5163
  }
5041
5164
  }
5042
5165
  const holes = [];
@@ -5073,8 +5196,8 @@ async function rubber(img, o, seed) {
5073
5196
  image(gap + line2 * 3, gap + line2 * 3, iw, ih, dataUri2(inked, "image/png"))
5074
5197
  ];
5075
5198
  if (band) {
5076
- const fs9 = await fitSize(caption, W - line2 * 10, Math.round(band * 0.5), 700);
5077
- body.push(text2(W / 2, fromMiddle(H - line2 * 3 - band / 2 - gap * 0.3, fs9), caption, { size: fs9, weight: 700, fill: o.ink, anchor: "middle" }));
5199
+ const fs10 = await fitSize(caption, W - line2 * 10, Math.round(band * 0.5), 700);
5200
+ body.push(text2(W / 2, fromMiddle(H - line2 * 3 - band / 2 - gap * 0.3, fs10), caption, { size: fs10, weight: 700, fill: o.ink, anchor: "middle" }));
5078
5201
  }
5079
5202
  const png = await rasterise(svgDoc(W, H, body.join(""))).png().toBuffer();
5080
5203
  return wearOut(png, distress(W, H, o.wear / 100, seed));
@@ -5095,10 +5218,10 @@ async function seal(img, o, seed) {
5095
5218
  ];
5096
5219
  const ringText = o.caption.trim().toUpperCase();
5097
5220
  const ringR = (c - line2 + inner) / 2;
5098
- const fs9 = Math.round((c - line2 - inner) * 0.52);
5221
+ const fs10 = Math.round((c - line2 - inner) * 0.52);
5099
5222
  const chars = [...ringText];
5100
5223
  const widths2 = [];
5101
- for (const ch of chars) widths2.push(await measure(ch, fs9, 700));
5224
+ for (const ch of chars) widths2.push(await measure(ch, fs10, 700));
5102
5225
  const total = widths2.reduce((a2, b) => a2 + b, 0);
5103
5226
  const spread = chars.length ? Math.min(Math.PI * 1.7, total * 1.18 / ringR) : 0;
5104
5227
  let a = -Math.PI / 2 - spread / 2;
@@ -5107,13 +5230,13 @@ async function seal(img, o, seed) {
5107
5230
  a += step / 2;
5108
5231
  const x = c + Math.cos(a) * ringR;
5109
5232
  const y = c + Math.sin(a) * ringR;
5110
- body.push(`<g transform="translate(${n(x)},${n(y)}) rotate(${n((a + Math.PI / 2) * 180 / Math.PI)})">${text2(0, fromMiddle(0, fs9), ch, { size: fs9, weight: 700, fill: o.ink, anchor: "middle" })}</g>`);
5233
+ body.push(`<g transform="translate(${n(x)},${n(y)}) rotate(${n((a + Math.PI / 2) * 180 / Math.PI)})">${text2(0, fromMiddle(0, fs10), ch, { size: fs10, weight: 700, fill: o.ink, anchor: "middle" })}</g>`);
5111
5234
  a += step / 2;
5112
5235
  });
5113
5236
  const star = (ang) => {
5114
5237
  const x = c + Math.cos(ang) * ringR;
5115
5238
  const y = c + Math.sin(ang) * ringR;
5116
- const R = fs9 * 0.4;
5239
+ const R = fs10 * 0.4;
5117
5240
  const pts = Array.from({ length: 10 }, (_, i) => {
5118
5241
  const rr2 = i % 2 ? R * 0.42 : R;
5119
5242
  const t2 = -Math.PI / 2 + i * Math.PI / 5;
@@ -6057,7 +6180,7 @@ ${notes.join("\n")}` : ""
6057
6180
  const r = { x, y, w: cols.start[tile.col + tile.colSpan - 1] + cols.size[tile.col + tile.colSpan - 1] - x, h: rows.start[tile.row + tile.rowSpan - 1] + rows.size[tile.row + tile.rowSpan - 1] - y };
6058
6181
  if (r.w < 1 || r.h < 1) continue;
6059
6182
  defs.push(`<clipPath id="t${i}"><path d="${rr(r.x, r.y, r.w, r.h, Math.max(0, Math.min(a.radius, r.w / 2, r.h / 2)))}"/></clipPath>`);
6060
- const parts = [`<rect x="${n(r.x)}" y="${n(r.y)}" width="${n(r.w)}" height="${n(r.h)}" fill="${tile.fill}"/>`];
6183
+ const parts = tile.kind === "photo" && tile.file ? [] : [`<rect x="${n(r.x)}" y="${n(r.y)}" width="${n(r.w)}" height="${n(r.h)}" fill="${tile.fill}"/>`];
6061
6184
  if (tile.kind === "photo") {
6062
6185
  if (tile.file) {
6063
6186
  let href;
@@ -6220,7 +6343,12 @@ ${notes.join("\n")}` : "");
6220
6343
  }
6221
6344
 
6222
6345
  // src/server.ts
6223
- var VERSION = "0.2.1";
6346
+ var VERSION = "0.2.2";
6347
+ if (process.argv[2] === "setup") {
6348
+ const { setup: setup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
6349
+ await setup2(process.argv.slice(3));
6350
+ process.exit(0);
6351
+ }
6224
6352
  var server = new McpServer({ name: "one-design", title: "One Design", version: VERSION, websiteUrl: "https://one.design" }, { instructions: INSTRUCTIONS });
6225
6353
  var t = toolkit(server);
6226
6354
  for (const m of [basics_exports, optimise_exports, privacy_docs_exports, brand_exports, account_exports]) m.register(t);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Every one.design tool for Claude and other MCP apps: compress, resize, convert, privacy, brand, store screenshots, PDFs and more, all on your own computer.",
5
5
  "type": "module",
6
6
  "bin": {