logomarks 0.0.0-stage → 1.0.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 (174) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +56 -2
  4. package/bin/logomarks.js +4 -0
  5. package/data/fonts/BricolageGrotesque-SemiBold.ttf +0 -0
  6. package/data/fonts/Cagliostro-Regular.ttf +0 -0
  7. package/data/fonts/Fraunces-SemiBold.ttf +0 -0
  8. package/data/fonts/IBMPlexMono-Regular.ttf +0 -0
  9. package/data/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  10. package/data/fonts/InstrumentSans-SemiBold.ttf +0 -0
  11. package/data/fonts/InstrumentSerif-Regular.ttf +0 -0
  12. package/data/fonts/Inter-SemiBold.ttf +0 -0
  13. package/data/fonts/Limelight-Regular.ttf +0 -0
  14. package/data/fonts/OFL.txt +103 -0
  15. package/data/fonts/Shrikhand-Regular.ttf +0 -0
  16. package/data/fonts/SpaceGrotesk-SemiBold.ttf +0 -0
  17. package/data/index.bin +0 -0
  18. package/docs/api.md +31 -0
  19. package/docs/commands.md +412 -0
  20. package/docs/editing.md +61 -0
  21. package/docs/mcp.md +45 -0
  22. package/package.json +94 -4
  23. package/src/brand/brand.d.ts +23 -0
  24. package/src/brand/brand.js +29 -0
  25. package/src/brand/fonts.d.ts +61 -0
  26. package/src/brand/fonts.js +93 -0
  27. package/src/brand/mark.d.ts +53 -0
  28. package/src/brand/mark.js +151 -0
  29. package/src/checks/finding.d.ts +56 -0
  30. package/src/checks/finding.js +57 -0
  31. package/src/checks/fit.d.ts +5 -0
  32. package/src/checks/fit.js +46 -0
  33. package/src/checks/index.d.ts +33 -0
  34. package/src/checks/index.js +57 -0
  35. package/src/checks/specks.d.ts +5 -0
  36. package/src/checks/specks.js +38 -0
  37. package/src/checks/widths.d.ts +7 -0
  38. package/src/checks/widths.js +48 -0
  39. package/src/cli/args.d.ts +20 -0
  40. package/src/cli/args.js +35 -0
  41. package/src/cli/commands/check.d.ts +10 -0
  42. package/src/cli/commands/check.js +66 -0
  43. package/src/cli/commands/combine.d.ts +10 -0
  44. package/src/cli/commands/combine.js +54 -0
  45. package/src/cli/commands/cover.d.ts +10 -0
  46. package/src/cli/commands/cover.js +53 -0
  47. package/src/cli/commands/edit.d.ts +10 -0
  48. package/src/cli/commands/edit.js +70 -0
  49. package/src/cli/commands/find.d.ts +10 -0
  50. package/src/cli/commands/find.js +61 -0
  51. package/src/cli/commands/get.d.ts +10 -0
  52. package/src/cli/commands/get.js +57 -0
  53. package/src/cli/commands/glitch.d.ts +10 -0
  54. package/src/cli/commands/glitch.js +57 -0
  55. package/src/cli/commands/index.d.ts +15 -0
  56. package/src/cli/commands/index.js +24 -0
  57. package/src/cli/commands/kit.d.ts +10 -0
  58. package/src/cli/commands/kit.js +66 -0
  59. package/src/cli/commands/mcp.d.ts +9 -0
  60. package/src/cli/commands/mcp.js +36 -0
  61. package/src/cli/commands/mockup.d.ts +10 -0
  62. package/src/cli/commands/mockup.js +61 -0
  63. package/src/cli/commands/parts.d.ts +10 -0
  64. package/src/cli/commands/parts.js +44 -0
  65. package/src/cli/commands/sheet.d.ts +10 -0
  66. package/src/cli/commands/sheet.js +66 -0
  67. package/src/cli/commands/similar.d.ts +10 -0
  68. package/src/cli/commands/similar.js +111 -0
  69. package/src/cli/commands/social.d.ts +10 -0
  70. package/src/cli/commands/social.js +65 -0
  71. package/src/cli/commands/specks.d.ts +20 -0
  72. package/src/cli/commands/specks.js +103 -0
  73. package/src/cli/edited.d.ts +6 -0
  74. package/src/cli/edited.js +32 -0
  75. package/src/cli/files.d.ts +4 -0
  76. package/src/cli/files.js +27 -0
  77. package/src/cli/help.d.ts +20 -0
  78. package/src/cli/help.js +55 -0
  79. package/src/cli/index.d.ts +10 -0
  80. package/src/cli/index.js +104 -0
  81. package/src/cli/options.d.ts +30 -0
  82. package/src/cli/options.js +155 -0
  83. package/src/cli/report.d.ts +4 -0
  84. package/src/cli/report.js +28 -0
  85. package/src/cli/style.d.ts +4 -0
  86. package/src/cli/style.js +34 -0
  87. package/src/cover/index.d.ts +49 -0
  88. package/src/cover/index.js +63 -0
  89. package/src/edit/combine.d.ts +31 -0
  90. package/src/edit/combine.js +103 -0
  91. package/src/edit/doc.d.ts +34 -0
  92. package/src/edit/doc.js +96 -0
  93. package/src/edit/glitch.d.ts +48 -0
  94. package/src/edit/glitch.js +43 -0
  95. package/src/edit/index.d.ts +8 -0
  96. package/src/edit/index.js +11 -0
  97. package/src/edit/ops.d.ts +51 -0
  98. package/src/edit/ops.js +90 -0
  99. package/src/edit/preview.d.ts +8 -0
  100. package/src/edit/preview.js +51 -0
  101. package/src/find/index.d.ts +54 -0
  102. package/src/find/index.js +88 -0
  103. package/src/index.d.ts +30 -0
  104. package/src/index.js +28 -0
  105. package/src/kit/index.d.ts +26 -0
  106. package/src/kit/index.js +31 -0
  107. package/src/mcp/content.d.ts +30 -0
  108. package/src/mcp/content.js +56 -0
  109. package/src/mcp/edited.d.ts +5 -0
  110. package/src/mcp/edited.js +25 -0
  111. package/src/mcp/index.d.ts +41 -0
  112. package/src/mcp/index.js +72 -0
  113. package/src/mcp/inputs.d.ts +46 -0
  114. package/src/mcp/inputs.js +122 -0
  115. package/src/mcp/preview.d.ts +4 -0
  116. package/src/mcp/preview.js +24 -0
  117. package/src/mcp/tools/check.d.ts +15 -0
  118. package/src/mcp/tools/check.js +47 -0
  119. package/src/mcp/tools/combine.d.ts +15 -0
  120. package/src/mcp/tools/combine.js +42 -0
  121. package/src/mcp/tools/cover.d.ts +20 -0
  122. package/src/mcp/tools/cover.js +46 -0
  123. package/src/mcp/tools/edit.d.ts +21 -0
  124. package/src/mcp/tools/edit.js +73 -0
  125. package/src/mcp/tools/find.d.ts +14 -0
  126. package/src/mcp/tools/find.js +36 -0
  127. package/src/mcp/tools/get.d.ts +15 -0
  128. package/src/mcp/tools/get.js +44 -0
  129. package/src/mcp/tools/glitch.d.ts +12 -0
  130. package/src/mcp/tools/glitch.js +39 -0
  131. package/src/mcp/tools/index.d.ts +4 -0
  132. package/src/mcp/tools/index.js +23 -0
  133. package/src/mcp/tools/kit.d.ts +28 -0
  134. package/src/mcp/tools/kit.js +51 -0
  135. package/src/mcp/tools/mockup.d.ts +22 -0
  136. package/src/mcp/tools/mockup.js +51 -0
  137. package/src/mcp/tools/sheet.d.ts +15 -0
  138. package/src/mcp/tools/sheet.js +43 -0
  139. package/src/mcp/tools/similar.d.ts +13 -0
  140. package/src/mcp/tools/similar.js +42 -0
  141. package/src/mcp/tools/social.d.ts +5 -0
  142. package/src/mcp/tools/social.js +68 -0
  143. package/src/mockup/index.d.ts +52 -0
  144. package/src/mockup/index.js +152 -0
  145. package/src/raster/render.d.ts +30 -0
  146. package/src/raster/render.js +100 -0
  147. package/src/rules.d.ts +73 -0
  148. package/src/rules.js +139 -0
  149. package/src/sheet/card.d.ts +20 -0
  150. package/src/sheet/card.js +190 -0
  151. package/src/sheet/draw.d.ts +26 -0
  152. package/src/sheet/draw.js +94 -0
  153. package/src/sheet/grid.d.ts +2 -0
  154. package/src/sheet/grid.js +32 -0
  155. package/src/sheet/index.d.ts +31 -0
  156. package/src/sheet/index.js +47 -0
  157. package/src/sheet/theme.d.ts +8 -0
  158. package/src/sheet/theme.js +45 -0
  159. package/src/similar/folder.d.ts +20 -0
  160. package/src/similar/folder.js +66 -0
  161. package/src/similar/nearest.d.ts +32 -0
  162. package/src/similar/nearest.js +60 -0
  163. package/src/similar/signature.d.ts +12 -0
  164. package/src/similar/signature.js +42 -0
  165. package/src/similar/store.d.ts +67 -0
  166. package/src/similar/store.js +88 -0
  167. package/src/util/color.d.ts +2 -0
  168. package/src/util/color.js +23 -0
  169. package/src/util/files.d.ts +8 -0
  170. package/src/util/files.js +60 -0
  171. package/src/util/round.d.ts +2 -0
  172. package/src/util/round.js +14 -0
  173. package/src/util/version.d.ts +2 -0
  174. package/src/util/version.js +10 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @module logomarks/mcp/inputs
3
+ *
4
+ * The inputs the tools share: their schemas (zod, which the server turns
5
+ * into JSON Schema for the caller), and reading them: a mark, a colour, the
6
+ * marks to check, the mark an edit starts from, and where to write. Relative paths resolve against the
7
+ * server's working folder (the project the agent is in).
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { z } from 'zod';
13
+ import { fitMark } from '@logomarks/core/fit';
14
+ import { PLATFORMS, STYLE_IDS } from '@logomarks/brand-kit';
15
+ import { loadMark } from '../brand/mark.js';
16
+ import { parseColor } from '../util/color.js';
17
+ import { FONTS } from '../brand/fonts.js';
18
+ import { expandSvgs, slugOf } from '../util/files.js';
19
+
20
+ /** The schemas the tools share. */
21
+ export const schema = {
22
+ mark: z.string().min(1).describe('The mark: a live slug ("hex-gem") or name ("Hex Gem") from find_marks, or the path to your own .svg file (fitted to the 512 grid first).'),
23
+ color: z.string().optional().describe('The brand colour as hex, e.g. "#ff5c3a". Default "#d4ff3a" (the Logomarks lime).'),
24
+ font: z.enum(/** @type {[string, ...string[]]} */ (Object.keys(FONTS))).optional().describe('The typeface for the brand name (all SIL OFL). Defaults to inter.'),
25
+ name: z.string().optional().describe("The brand's name, set beside the mark. Defaults to the mark's name."),
26
+ svg: z.string().optional().describe('One mark as SVG text. Use this or paths.'),
27
+ svgName: z.string().optional().describe('A name for the svg (its slug), shown in the results. Defaults to "mark".'),
28
+ paths: z.array(z.string()).optional().describe('.svg files or folders of them (not recursive). Use this or svg.'),
29
+ ignore: z.array(z.string()).optional().describe('Finding ids to leave out, e.g. ["uneven-gaps"].'),
30
+ strict: z.boolean().optional().describe('Count warnings as failures too. Default false.'),
31
+ seed: z.string().optional().describe('Drives every random choice; the same seed gives the same image. Defaults to the mark\'s slug.'),
32
+ source: z.string().optional().describe('The mark: a live slug or name, or the path to an .svg file. Use this or svg.'),
33
+ sourceSvg: z.string().optional().describe('The mark as SVG text (fitted to the 512 grid first). Use this or mark.'),
34
+ out: z.string().optional().describe('Also write the result here (.svg); the result then gives the path instead of the SVG text. Relative to the working folder; an existing file is replaced.'),
35
+ tagline: z.string().optional().describe('A short line under the name on social images and the brand sheet.'),
36
+ url: z.string().optional().describe("The brand's website, for the brand sheet and the email signature."),
37
+ handle: z.string().optional().describe('The social handle, e.g. "@hexalabs". Defaults to @ and the name.'),
38
+ platforms: z.array(z.string()).optional().describe(`Social images by id or platform name (default every one): ${Object.keys(PLATFORMS).join(', ')}; or ${[...new Set(Object.values(PLATFORMS).map((p) => p.platform))].join(', ')}.`),
39
+ styles: z.array(z.enum(/** @type {[import('@logomarks/brand-kit').StyleId, ...import('@logomarks/brand-kit').StyleId[]]} */ ([...STYLE_IDS]))).optional().describe('minimal (the logo on paper), bold (the brand colour edge to edge, the mark cropped large), pattern (the mark repeated), gradient (a grainy gradient of the brand colour; only when asked for). Default the first three.'),
40
+ animated: z.boolean().optional().describe('Also write animated SVGs for avatars, stories and banners. Default false.'),
41
+ };
42
+
43
+ /**
44
+ * A path, resolved against the working folder.
45
+ *
46
+ * @param {string} cwd The working folder.
47
+ * @param {string} p The path.
48
+ * @returns {string} The absolute path.
49
+ */
50
+ export const resolvePath = (cwd, p) => path.resolve(cwd, p);
51
+
52
+ /**
53
+ * A mark by slug, name or file (a relative file resolved against the working folder).
54
+ *
55
+ * @param {string} ref The reference.
56
+ * @param {string} cwd The working folder.
57
+ * @returns {import('../brand/mark.js').Mark} The mark.
58
+ * @throws {import('../brand/mark.js').MarkNotFound} When it is neither.
59
+ */
60
+ export function readMark(ref, cwd) {
61
+ const file = resolvePath(cwd, ref);
62
+ return loadMark(fs.existsSync(file) ? file : ref);
63
+ }
64
+
65
+ /**
66
+ * A colour input.
67
+ *
68
+ * @param {string | undefined} value The value.
69
+ * @returns {string | undefined} `#RRGGBB`, or undefined when it wasn't given.
70
+ * @throws {Error} When it isn't a colour.
71
+ */
72
+ export function readColor(value) {
73
+ if (value === undefined) return undefined;
74
+ const hex = parseColor(value);
75
+ if (!hex) throw new Error(`color takes a hex colour such as "#ff5c3a", not "${value}"`);
76
+ return hex;
77
+ }
78
+
79
+ /**
80
+ * The marks a check names: one as SVG text, or files and folders.
81
+ *
82
+ * @param {{ svg?: string, svgName?: string, paths?: string[] }} input The inputs.
83
+ * @param {string} cwd The working folder.
84
+ * @returns {import('./inputs.js').SvgInput[]} Each mark's name, file (absolute; none for text) and SVG.
85
+ * @throws {Error} When neither or both are given, a path is missing, or a folder has no SVGs.
86
+ */
87
+ export function readSvgs({ svg, svgName, paths }, cwd) {
88
+ if (svg !== undefined && paths?.length) throw new Error('pass svg or paths, not both');
89
+ if (svg !== undefined) return [{ name: svgName?.trim() || 'mark', svg }];
90
+ if (!paths?.length) throw new Error('pass svg (the SVG text) or paths (.svg files or folders)');
91
+ const files = expandSvgs(paths.map((p) => resolvePath(cwd, p)));
92
+ if (!files.length) throw new Error(`no .svg files in ${paths.join(', ')}`);
93
+ return files.map((file) => ({ name: slugOf(file), file, svg: fs.readFileSync(file, 'utf8') }));
94
+ }
95
+
96
+ /**
97
+ * A mark as SVG from a live slug or name, an .svg path, or SVG text (fitted to the grid).
98
+ *
99
+ * @param {string} ref The reference, or the SVG itself.
100
+ * @param {string} cwd The working folder.
101
+ * @returns {{ name: string, svg: string }} Its name and SVG.
102
+ * @throws {import('../brand/mark.js').MarkNotFound} When it is none of them.
103
+ */
104
+ export function readAnyMark(ref, cwd) {
105
+ if (ref.trimStart().startsWith('<')) return { name: 'mark', svg: fitMark(ref).svg };
106
+ const m = readMark(ref, cwd);
107
+ return { name: m.slug, svg: m.svg };
108
+ }
109
+
110
+ /**
111
+ * The mark an edit starts from: a live mark or file (`mark`), or SVG text (`svg`).
112
+ *
113
+ * @param {{ mark?: string, svg?: string }} input The inputs.
114
+ * @param {string} cwd The working folder.
115
+ * @returns {{ name: string, svg: string }} Its name and SVG, fitted.
116
+ * @throws {Error} When neither or both are given.
117
+ */
118
+ export function readSource({ mark, svg }, cwd) {
119
+ if (mark !== undefined && svg !== undefined) throw new Error('pass mark or svg, not both');
120
+ if (mark === undefined && svg === undefined) throw new Error('pass mark (a live slug or name, or an .svg path) or svg (the SVG text)');
121
+ return mark !== undefined ? readAnyMark(mark, cwd) : { name: 'mark', svg: fitMark(/** @type {string} */ (svg)).svg };
122
+ }
@@ -0,0 +1,4 @@
1
+ /** The longest side of an image handed back, in pixels: 800. */
2
+ export const PREVIEW: number;
3
+ /** The PNG itself when its longest side is within `PREVIEW`, else the SVG drawn again to it. */
4
+ export function preview(img: { png: Uint8Array; svg: string; width: number; height: number }): Promise<Uint8Array>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @module logomarks/mcp/preview
3
+ *
4
+ * The image a tool hands back to look at: no larger than it needs to be.
5
+ * Mockups and covers carry textures and gradients, so a full-size PNG runs
6
+ * to megabytes; the agent gets a preview, and the file gets the full size.
7
+ */
8
+
9
+ import { render } from '../raster/render.js';
10
+
11
+ /** The longest side of an image handed back, in pixels. */
12
+ export const PREVIEW = 800;
13
+
14
+ /**
15
+ * An image to hand back: the PNG itself when it is small enough, else the SVG drawn again with its longest side at `PREVIEW`.
16
+ *
17
+ * @param {{ png: Uint8Array, svg: string, width: number, height: number }} img The image and its SVG.
18
+ * @returns {Promise<Uint8Array>} The PNG to return.
19
+ */
20
+ export async function preview({ png, svg, width, height }) {
21
+ const longest = Math.max(width, height);
22
+ if (longest <= PREVIEW) return png;
23
+ return /** @type {Uint8Array} */ ((await render(svg, { width: Math.round((width * PREVIEW) / longest), text: true, png: true })).png);
24
+ }
@@ -0,0 +1,15 @@
1
+ import type { Tool } from '../index.js';
2
+
3
+ /** `check_mark`'s arguments: `svg` text or `paths`. */
4
+ export interface CheckInput {
5
+ svg?: string;
6
+ svgName?: string;
7
+ paths?: string[];
8
+ /** Refit to the grid (files in place; text returned). */
9
+ fix?: boolean;
10
+ strict?: boolean;
11
+ ignore?: string[];
12
+ }
13
+
14
+ /** `check_mark`: every house rule, findings as JSON. */
15
+ export const tool: Tool<CheckInput>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/check
3
+ *
4
+ * `check_mark`: every house rule on one SVG or a folder of them, with
5
+ * finding ids, messages and boxes; and the safe refit, when asked.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import { z } from 'zod';
10
+ import { check, fix } from '../../checks/index.js';
11
+ import { schema, readSvgs } from '../inputs.js';
12
+ import { result, text } from '../content.js';
13
+
14
+ /** @type {import('../index.js').Tool<import('./check.js').CheckInput>} */
15
+ export const tool = {
16
+ name: 'check_mark',
17
+ title: 'Check a mark against the house rules',
18
+ description: [
19
+ 'Check SVG logomarks against the house rules; use it on every mark you draw or edit, because reading SVG code can\'t show these.',
20
+ 'Errors: viewbox, size, centre, clipped (fit to the 512 canvas, longest side 448, centred: fix=true refits), markup (filled paths only), empty, speck, sliver, pinhole (stray pieces and holes under 60 / 30 sq units, each with its box).',
21
+ 'Warnings: fill (currentColor), precision (one decimal), thin and narrow-gap (under 10 units: lost at 32 px), uneven-gaps (keep one gap, 22, or a clear 1:2).',
22
+ 'Returns { ok, passed, failed, marks: [{ name, ok, errors, warnings, size, findings: [{ id, severity, message, box }] }] }.',
23
+ 'Fix every error, read every warning, then run similar_marks and review_sheet. Re-check until ok.',
24
+ ].join(' '),
25
+ inputSchema: {
26
+ svg: schema.svg,
27
+ svgName: schema.svgName,
28
+ paths: schema.paths,
29
+ fix: z.boolean().optional().describe('Refit to the grid (the only automatic fix): files in paths are rewritten in place; for svg text the refitted SVG is returned. Default false.'),
30
+ strict: schema.strict,
31
+ ignore: schema.ignore,
32
+ },
33
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
34
+ async run({ svg, svgName, paths, fix: refit = false, strict = false, ignore = [] }, { cwd }) {
35
+ const marks = readSvgs({ svg, svgName, paths }, cwd).map(({ name, file, svg: source }) => {
36
+ let report = check(source, { name, ignore, strict });
37
+ const fitted = refit ? fix(source, report) : null;
38
+ if (fitted) {
39
+ if (file) fs.writeFileSync(file, fitted);
40
+ report = check(fitted, { name, ignore, strict });
41
+ }
42
+ return { name, ...(file && { file }), fixed: Boolean(fitted), ...report, ...(fitted && !file && { svg: fitted }) };
43
+ });
44
+ const failed = marks.filter((m) => !m.ok).length;
45
+ return result(text({ ok: !failed, passed: marks.length - failed, failed, marks }));
46
+ },
47
+ };
@@ -0,0 +1,15 @@
1
+ import type { Tool } from '../index.js';
2
+ import type { CombineOp, Placement } from '../../edit/combine.js';
3
+
4
+ /** `combine_marks`'s arguments. */
5
+ export interface CombineInput {
6
+ /** A live slug or name, an .svg path, or SVG text. */
7
+ a: string;
8
+ b: string;
9
+ op: CombineOp;
10
+ transform?: Placement;
11
+ out?: string;
12
+ }
13
+
14
+ /** `combine_marks`: two marks made one, fitted, checked and shown. */
15
+ export const tool: Tool<CombineInput>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/combine
3
+ *
4
+ * `combine_marks`: two marks made one, fitted and checked.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import { combineMarks, COMBINE_OPS } from '../../edit/combine.js';
9
+ import { readAnyMark, schema } from '../inputs.js';
10
+ import { edited } from '../edited.js';
11
+
12
+ /** A mark, by any means. */
13
+ const markRef = z.string().min(1);
14
+
15
+ /** @type {import('../index.js').Tool<import('./combine.js').CombineInput>} */
16
+ export const tool = {
17
+ name: 'combine_marks',
18
+ title: 'Combine two marks',
19
+ description: [
20
+ 'Combine two marks into one with the vector editor’s booleans (curves kept): union, subtract (b cut out of a), intersect, exclude (where exactly one covers), or place (both, as they are).',
21
+ 'b is placed on a’s 512 canvas first: transform scales and turns it about its own centre, flips it, then moves it by x, y units. Both are fitted to the grid first, so scale 0.5 makes b half a’s size.',
22
+ 'Returns { ok, svg (512 canvas, longest side 448, centred; the file path instead with out), checks } and the result as an image.',
23
+ ].join(' '),
24
+ inputSchema: {
25
+ a: markRef.describe('The first mark: a live slug or name, an .svg path, or SVG text.'),
26
+ b: markRef.describe('The second, the same ways.'),
27
+ op: z.enum(/** @type {[import('../../edit/combine.js').CombineOp, ...import('../../edit/combine.js').CombineOp[]]} */ ([...COMBINE_OPS])).describe(`How to combine them: ${COMBINE_OPS.join(', ')}.`),
28
+ transform: z.object({
29
+ scale: z.number().positive().optional().describe('Scale b about its own centre. Default 1.'),
30
+ rotate: z.number().optional().describe('Turn b clockwise about its own centre, in degrees. Default 0.'),
31
+ flip: z.enum(['horizontal', 'vertical']).optional().describe('Flip b. Default: no flip.'),
32
+ x: z.number().optional().describe('Move b across, in units. Default 0.'),
33
+ y: z.number().optional().describe('Move b down, in units. Default 0.'),
34
+ }).optional().describe('Where b goes over a. Default: centred, as it is.'),
35
+ out: schema.out,
36
+ },
37
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
38
+ async run({ a, b, op, transform, out }, { cwd }) {
39
+ const [first, second] = [readAnyMark(a, cwd), readAnyMark(b, cwd)];
40
+ return edited(combineMarks(first.svg, second.svg, { op, transform, name: `${first.name}-${op}-${second.name}` }), { out, cwd });
41
+ },
42
+ };
@@ -0,0 +1,20 @@
1
+ import type { Tool } from '../index.js';
2
+
3
+ /** `render_cover`'s arguments. */
4
+ export interface CoverInput {
5
+ mark: string;
6
+ /** One of `RECIPES`. */
7
+ recipe?: string;
8
+ title?: string;
9
+ label?: string;
10
+ color?: string;
11
+ /** `og`, `w:h` or a number. */
12
+ aspect?: string;
13
+ width?: number;
14
+ seed?: string;
15
+ /** Also write it here (.png or .svg). */
16
+ out?: string;
17
+ }
18
+
19
+ /** `render_cover`: a cover with the mark on it, as an image. */
20
+ export const tool: Tool<CoverInput>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/cover
3
+ *
4
+ * `render_cover`: a cover or social image with the mark on it, as an image;
5
+ * written to a file when asked.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import { cover, RECIPES } from '../../cover/index.js';
10
+ import { writeFile } from '../../util/files.js';
11
+ import { schema, readMark, readColor, resolvePath } from '../inputs.js';
12
+ import { result, text, image } from '../content.js';
13
+ import { preview, PREVIEW } from '../preview.js';
14
+
15
+ /** Each recipe and what it looks like, for the description. */
16
+ const recipes = Object.entries(RECIPES).map(([k, r]) => `${k} (${r.about.replace(/\.$/, '')})`).join('; ');
17
+
18
+ /** @type {import('../index.js').Tool<import('./cover.js').CoverInput>} */
19
+ export const tool = {
20
+ name: 'render_cover',
21
+ title: 'Render a cover image',
22
+ description: [
23
+ 'Draw a cover with the mark on it and return the image; use it for an Open Graph image, a blog or launch header, or a square post.',
24
+ `Recipes: ${recipes}.`,
25
+ `Returns a preview up to ${PREVIEW} px; pass out to write the full-size file (.png, or .svg with the text as outlines).`,
26
+ ].join(' '),
27
+ inputSchema: {
28
+ mark: schema.mark,
29
+ recipe: z.enum(/** @type {[string, ...string[]]} */ (Object.keys(RECIPES))).optional().describe(`The look: ${Object.keys(RECIPES).join(', ')}. Default mark.`),
30
+ title: z.string().optional().describe('A headline; the mark moves out of its way.'),
31
+ label: z.string().optional().describe('A small line over the title.'),
32
+ color: schema.color,
33
+ aspect: z.string().regex(/^(og|\d+(\.\d+)?(:\d+(\.\d+)?)?)$/, 'og, w:h (16:9) or a number').optional().describe('og (1200×630, the default), 1:1, 16:9, 4:5, 9:16, any w:h, or a number.'),
34
+ width: z.number().int().min(200).max(6000).optional().describe("The written file's width in pixels. Default 1200."),
35
+ seed: schema.seed,
36
+ out: z.string().optional().describe('Also write the full-size file here: .png, or .svg (text as outlines). Relative to the working folder; an existing file is replaced.'),
37
+ },
38
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
39
+ async run({ mark: ref, recipe, title, label, color, aspect, width, seed, out }, { cwd }) {
40
+ const mark = readMark(ref, cwd);
41
+ const file = out && resolvePath(cwd, out);
42
+ const c = await cover({ mark, recipe, title, label, seed, color: readColor(color), aspect: aspect && /^[\d.]+$/.test(aspect) ? Number(aspect) : aspect, width: file ? width : Math.min(width ?? PREVIEW, PREVIEW) });
43
+ if (file) writeFile(file, file.toLowerCase().endsWith('.svg') ? c.svg : c.png);
44
+ return result(text({ recipe: recipe ?? 'mark', ...(file && { file, width: c.width, height: c.height }) }), image(await preview(c)));
45
+ },
46
+ };
@@ -0,0 +1,21 @@
1
+ import type { Tool } from '../index.js';
2
+ import type { EditOp } from '../../edit/ops.js';
3
+
4
+ /** `edit_mark`'s arguments. */
5
+ export interface EditInput {
6
+ mark?: string;
7
+ svg?: string;
8
+ ops: EditOp[];
9
+ out?: string;
10
+ }
11
+
12
+ /** `list_parts`'s arguments. */
13
+ export interface PartsInput {
14
+ mark?: string;
15
+ svg?: string;
16
+ }
17
+
18
+ /** `edit_mark`: ops by part, run in order, the result fitted, checked and shown. */
19
+ export const tool: Tool<EditInput>;
20
+ /** `list_parts`: the parts by index, and an image of them numbered. */
21
+ export const parts: Tool<PartsInput>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/edit
3
+ *
4
+ * `edit_mark`: a list of edits by part, run in order, the result fitted and
5
+ * checked; and `list_parts`, the parts by index, to name them.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import { editMark, markParts, OPS } from '../../edit/ops.js';
10
+ import { partsPreview } from '../../edit/preview.js';
11
+ import { schema, readSource } from '../inputs.js';
12
+ import { edited } from '../edited.js';
13
+ import { result, text, image } from '../content.js';
14
+
15
+ /** A point on the 512 canvas. */
16
+ const point = z.tuple([z.number(), z.number()]);
17
+
18
+ /** One op. */
19
+ const op = z.object({
20
+ op: z.enum(/** @type {[import('../../edit/ops.js').OpName, ...import('../../edit/ops.js').OpName[]]} */ (Object.keys(OPS))),
21
+ parts: z.array(z.number().int().min(0)).optional().describe('Part indices from list_parts, as they stand when this op runs (a delete renumbers what follows). Default every part.'),
22
+ dx: z.number().optional().describe('move: units across.'),
23
+ dy: z.number().optional().describe('move: units down.'),
24
+ angle: z.number().optional().describe('rotate: degrees, clockwise.'),
25
+ by: z.union([z.number().positive(), z.tuple([z.number().positive(), z.number().positive()])]).optional().describe('scale: a factor, or [sx, sy].'),
26
+ about: point.optional().describe('rotate, scale, flip: the point to turn about, on the 512 canvas. Default the parts’ centre.'),
27
+ axis: z.enum(['horizontal', 'vertical', 'both']).optional().describe('flip: horizontal (left to right) or vertical. mirror: vertical (left matches right; the default), horizontal or both.'),
28
+ radius: z.number().min(0).optional().describe('round: the corner radius in units.'),
29
+ distance: z.number().optional().describe('offset: units to grow (positive) or shrink (negative).'),
30
+ join: z.enum(['miter', 'round']).optional().describe('offset: sharp corners (miter, the default) or round.'),
31
+ count: z.number().int().min(2).max(24).optional().describe('radial: copies about the centre (4).'),
32
+ color: z.string().optional().describe('color, tone: hex.'),
33
+ });
34
+
35
+ /** @type {import('../index.js').Tool<import('./edit.js').EditInput>} */
36
+ export const tool = {
37
+ name: 'edit_mark',
38
+ title: 'Edit a mark',
39
+ description: [
40
+ 'Edit a mark by part with a list of ops, run in order (the vector editor’s own operations: curves kept, booleans exact). Use it to change a live mark or fix one you drew.',
41
+ `Ops: ${Object.entries(OPS).map(([k, v]) => `${k} (${v.about})`).join('; ')}.`,
42
+ 'Call list_parts first to see each part’s index, box and area. Example: [{"op":"delete","parts":[3]},{"op":"round","radius":12},{"op":"mirror","axis":"vertical"}].',
43
+ 'Returns { ok, parts (after), svg (512 canvas, longest side 448, centred; the file path instead with out), checks } and the result as an image. Fix any speck, sliver or gap the checks report.',
44
+ ].join(' '),
45
+ inputSchema: {
46
+ mark: schema.source,
47
+ svg: schema.sourceSvg,
48
+ ops: z.array(op).min(1).describe('The edits, in order.'),
49
+ out: schema.out,
50
+ },
51
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
52
+ async run({ mark, svg, ops, out }, { cwd }) {
53
+ const source = readSource({ mark, svg }, cwd);
54
+ return edited(editMark(source.svg, ops, { name: source.name }), { out, cwd });
55
+ },
56
+ };
57
+
58
+ /** @type {import('../index.js').Tool<import('./edit.js').PartsInput>} */
59
+ export const parts = {
60
+ name: 'list_parts',
61
+ title: 'List a mark’s parts',
62
+ description: [
63
+ 'List a mark’s parts by index in drawing order; call it before edit_mark, whose ops name parts by these indices. Each part has its box [x0, y0, x1, y1] and centre on the 512 canvas, its area (holes taken out), its holes and its tone.',
64
+ 'Returns { count, parts } and an image of the mark with every part boxed and numbered.',
65
+ ].join(' '),
66
+ inputSchema: { mark: schema.source, svg: schema.sourceSvg },
67
+ annotations: { readOnlyHint: true, openWorldHint: false },
68
+ async run({ mark, svg }, { cwd }) {
69
+ const source = readSource({ mark, svg }, cwd);
70
+ const list = markParts(source.svg);
71
+ return result(text({ count: list.length, parts: list }), image(await partsPreview(source.svg, list)));
72
+ },
73
+ };
@@ -0,0 +1,14 @@
1
+ import type { Tool } from '../index.js';
2
+
3
+ /** `find_marks`'s arguments. */
4
+ export interface FindInput {
5
+ query?: string;
6
+ style?: string;
7
+ tags?: string[];
8
+ industry?: string;
9
+ letter?: string;
10
+ limit?: number;
11
+ }
12
+
13
+ /** `find_marks`: search the live set by words and filters (no arguments: the filters' values). */
14
+ export const tool: Tool<FindInput>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/find
3
+ *
4
+ * `find_marks`: search the bundled live set by words and filters.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import { findMarks, facets, DEFAULT_LIMIT } from '../../find/index.js';
9
+ import { result, text } from '../content.js';
10
+
11
+ /** @type {import('../index.js').Tool<import('./find.js').FindInput>} */
12
+ export const tool = {
13
+ name: 'find_marks',
14
+ title: 'Find live Logomarks',
15
+ description: [
16
+ 'Search the free Logomarks set (geometric logomarks, CC0, bundled offline): use it first when someone needs a logo, icon or brand mark, or a mark to build on.',
17
+ 'Search works as on logomarks.ai: every word must be found in a mark (its name, search words, industries, style, tags, mood, letter or description), trades by their UK and US names too ("solicitor" finds the lawyers\' marks); the best fits come first. Filler words (a, for, logo) are ignored.',
18
+ 'Filters narrow it exactly. Call with no arguments to list every style, tag, mood and industry with its count.',
19
+ 'Returns { total, marks: [{ slug, name, style, description, url … }] }: pass a slug to get_mark, build_brand_kit, render_mockup or render_cover.',
20
+ ].join(' '),
21
+ inputSchema: {
22
+ query: z.string().optional().describe('Free words: what it should look like or be for, e.g. "fox", "calm hexagon", "coffee", "letter k".'),
23
+ style: z.string().optional().describe('One style, e.g. letter, badge, monogram, iso, line, grid, glitch, animal, bold, modular, woven, inflated. Call with no arguments for every style.'),
24
+ tags: z.array(z.string()).optional().describe('Tags a mark must all have: round, square, symmetric, radial, solid, light, wide, tall, letter, number, new.'),
25
+ industry: z.string().optional().describe('An industry it suits, e.g. coffee, fintech, ai, law-firm, real-estate, fitness, music.'),
26
+ letter: z.string().max(2).optional().describe('The letter or number a letter mark shows, e.g. "K" or "7".'),
27
+ limit: z.number().int().min(1).max(100).optional().describe(`How many marks to return. Default ${DEFAULT_LIMIT}.`),
28
+ },
29
+ annotations: { readOnlyHint: true, openWorldHint: false },
30
+ async run({ query, style, tags, industry, letter, limit }) {
31
+ if (!query && !style && !tags?.length && !industry && !letter) return result(text({ facets: facets() }));
32
+ const found = findMarks(query, { style, tags, industry, letter, limit });
33
+ if (!found.total) return result(text({ ...found, hint: 'Nothing matched: try fewer or broader words, or call find_marks with no arguments to see the styles, tags and industries.' }));
34
+ return result(text(found));
35
+ },
36
+ };
@@ -0,0 +1,15 @@
1
+ import type { Tool } from '../index.js';
2
+
3
+ /** `get_mark`'s arguments. */
4
+ export interface GetInput {
5
+ /** A live slug or name, or an .svg path. */
6
+ mark: string;
7
+ color?: string;
8
+ /** Also return a PNG this many pixels square. */
9
+ size?: number;
10
+ /** Write the mark here (.svg or .png). */
11
+ out?: string;
12
+ }
13
+
14
+ /** `get_mark`: a mark's SVG, and a PNG to look at when asked. */
15
+ export const tool: Tool<GetInput>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/get
3
+ *
4
+ * `get_mark`: a mark's SVG, painted in a colour, or a PNG to look at; and
5
+ * written to a file when asked.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import { paint, markPng, MARK_URL } from '../../brand/mark.js';
10
+ import { writeFile } from '../../util/files.js';
11
+ import { schema, readMark, readColor, resolvePath } from '../inputs.js';
12
+ import { result, text, image } from '../content.js';
13
+
14
+ /** A PNG's side when a .png is written without one. */
15
+ const PNG_SIZE = 512;
16
+
17
+ /** @type {import('../index.js').Tool<import('./get.js').GetInput>} */
18
+ export const tool = {
19
+ name: 'get_mark',
20
+ title: 'Get a mark',
21
+ description: [
22
+ 'Get a mark to use: a live Logomark by slug or name (CC0, free to use), or your own .svg fitted to the 512 grid.',
23
+ 'The SVG is 512×512, longest side 448, centred, filled paths in currentColor (or in color, when given).',
24
+ 'Pass size to also get a PNG image to look at. Pass out to write the file (.svg, or .png at size, default 512); the result then gives its path instead of the SVG text.',
25
+ 'Returns { slug, name, url, licence, color, svg | file }.',
26
+ ].join(' '),
27
+ inputSchema: {
28
+ mark: schema.mark,
29
+ color: z.string().optional().describe('Paint it in this hex colour, e.g. "#ff5c3a". Default: currentColor (the SVG) or black (a PNG).'),
30
+ size: z.number().int().min(16).max(4096).optional().describe('Also return a PNG this many pixels square (16–4096), to look at. Default: none.'),
31
+ out: z.string().optional().describe('Write the mark here: .svg, or .png (at size, default 512). Relative to the working folder; an existing file is replaced.'),
32
+ },
33
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
34
+ async run({ mark: ref, color: c, size, out }, { cwd }) {
35
+ const mark = readMark(ref, cwd);
36
+ const color = readColor(c);
37
+ const svg = paint(mark.svg, color);
38
+ const png = size || out?.toLowerCase().endsWith('.png') ? await markPng(mark.svg, { size: size ?? PNG_SIZE, color: color ?? '#000000' }) : null;
39
+ const file = out && resolvePath(cwd, out);
40
+ if (file) writeFile(file, file.toLowerCase().endsWith('.png') ? /** @type {Uint8Array} */ (png) : svg);
41
+ const about = { slug: mark.slug, name: mark.name, ...(mark.live ? { url: MARK_URL + mark.slug, licence: 'CC0-1.0' } : { live: false }), color: color ?? 'currentColor', ...(file && { file }) };
42
+ return result(text(file ? about : { ...about, svg }), ...(png && size ? [image(png)] : []));
43
+ },
44
+ };
@@ -0,0 +1,12 @@
1
+ import type { Tool } from '../index.js';
2
+ import type { GlitchOptions } from '../../edit/glitch.js';
3
+
4
+ /** `glitch_mark`'s arguments: the mark (`mark` or `svg`), the preset and its settings, and where to write it. */
5
+ export interface GlitchInput extends Omit<GlitchOptions, 'name'> {
6
+ mark?: string;
7
+ svg?: string;
8
+ out?: string;
9
+ }
10
+
11
+ /** `glitch_mark`: a glitch edition, fitted and checked, and its image. */
12
+ export const tool: Tool<GlitchInput>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/glitch
3
+ *
4
+ * `glitch_mark`: a glitch edition of a mark, fitted and checked.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import { glitchMark, PRESETS, PRESET_ABOUT } from '../../edit/glitch.js';
9
+ import { schema, readSource } from '../inputs.js';
10
+ import { edited } from '../edited.js';
11
+
12
+ /** @type {import('../index.js').Tool<import('./glitch.js').GlitchInput>} */
13
+ export const tool = {
14
+ name: 'glitch_mark',
15
+ title: 'Glitch a mark',
16
+ description: [
17
+ 'Make a glitch edition of a mark: cut with the house gap (22) and the pieces slid, as the Logomarks glitch editions are. Use it to make a live mark more your own.',
18
+ `Presets: ${Object.entries(PRESET_ABOUT).map(([k, v]) => `${k} (${v})`).join('; ')}.`,
19
+ 'A seed places the cuts: the same seed gives the same SVG, byte for byte; another seed is a shuffle. A placement that would leave a speck or a sliver is nudged, then reseeded; when none is clean the call fails, so try another seed.',
20
+ 'Returns { ok, glitch, svg (512 canvas, longest side 448, centred; the file path instead with out), checks } and the result as an image: look at it.',
21
+ ].join(' '),
22
+ inputSchema: {
23
+ mark: schema.source,
24
+ svg: schema.sourceSvg,
25
+ preset: z.enum(/** @type {[import('../../edit/glitch.js').GlitchPreset, ...import('../../edit/glitch.js').GlitchPreset[]]} */ (Object.keys(PRESETS))).describe(`The kind of glitch: ${Object.keys(PRESETS).join(', ')}.`),
26
+ angle: z.number().min(-180).max(180).optional().describe('The cut direction in degrees, clockwise from across. Default: the mark’s own slope (shear), across the lanes (lane), else 0.'),
27
+ place: z.number().min(0).max(1).optional().describe('Where the first cut crosses, 0–1 across the mark. Default: seeded.'),
28
+ slide: z.number().min(0).max(200).optional().describe('How far the sides slide apart along the cut, in units; a band’s largest nudge. Default 44 (twice the gap).'),
29
+ gap: z.number().min(4).max(80).optional().describe('The gap at each cut, in units. Default 22 (the house gap).'),
30
+ cuts: z.number().int().min(1).max(5).optional().describe('Cuts for slice (1–3), bands for bands (3–5). Default: seeded.'),
31
+ seed: z.number().int().min(0).optional().describe('Places the cuts: the same seed gives the same SVG, another is a shuffle. Default 1.'),
32
+ out: schema.out,
33
+ },
34
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
35
+ async run({ mark, svg, out, ...o }, { cwd }) {
36
+ const source = readSource({ mark, svg }, cwd);
37
+ return edited(glitchMark(source.svg, { ...o, name: source.name }), { out, cwd });
38
+ },
39
+ };
@@ -0,0 +1,4 @@
1
+ import type { Tool } from '../index.js';
2
+
3
+ /** The tools, in order: find, get, check, similar, sheet; list_parts, glitch, edit, combine; build_brand_kit, build_social_pack, render_mockup, render_cover. */
4
+ export const TOOLS: Tool<never>[];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module logomarks/mcp/tools
3
+ *
4
+ * Every tool the MCP server offers, in the order an agent uses them: find
5
+ * and get a mark; check, compare and look at one it drew; edit it (glitch,
6
+ * edit by part, combine); build on it.
7
+ */
8
+
9
+ import { tool as find } from './find.js';
10
+ import { tool as get } from './get.js';
11
+ import { tool as check } from './check.js';
12
+ import { tool as similar } from './similar.js';
13
+ import { tool as sheet } from './sheet.js';
14
+ import { tool as glitch } from './glitch.js';
15
+ import { tool as edit, parts } from './edit.js';
16
+ import { tool as combine } from './combine.js';
17
+ import { tool as kit } from './kit.js';
18
+ import { tool as social } from './social.js';
19
+ import { tool as mockup } from './mockup.js';
20
+ import { tool as cover } from './cover.js';
21
+
22
+ /** The tools, in order. */
23
+ export const TOOLS = [find, get, check, similar, sheet, parts, glitch, edit, combine, kit, social, mockup, cover];