@coolicons/mcp 0.0.0 → 5.0.0-beta.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.
package/dist/server.js ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { createInterface } from "node:readline";
4
+ import { component, findIcon, iconSet, LookupError, RADII, SITE, STROKED, STYLES, search, suggest, svg, usage } from "./core.js";
5
+
6
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
7
+ const PROTOCOLS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
8
+
9
+ const INSTRUCTIONS = `coolicons is an open source icon set (MIT) in five styles and three corner radii. Never guess an icon name: resolve every name with search_icons before writing code, and look up several names in one call by separating them with commas. If nothing fits, tell the user instead of using a similar icon. Use get_usage for the exact import line, it applies the style and radius entry and the Icon suffix for names that clash with JavaScript or browser globals.`;
10
+
11
+ const style = { type: "string", enum: STYLES, description: "line, duocolor, duotone, fill or bulk. Default line." };
12
+ const radius = { type: "string", enum: RADII, description: "Corner radius: sharp, regular or round. Default regular." };
13
+ const readOnly = { readOnlyHint: true, openWorldHint: false };
14
+
15
+ const TOOLS = [
16
+ {
17
+ name: "describe_set",
18
+ title: "Describe coolicons",
19
+ description: "Counts, styles, corner radii, categories, packages and naming rules of coolicons. Call it once before choosing icons.",
20
+ inputSchema: { type: "object", properties: {} },
21
+ annotations: readOnly,
22
+ },
23
+ {
24
+ name: "search_icons",
25
+ title: "Search coolicons",
26
+ description:
27
+ "Find coolicons by name or meaning, ranked: exact name, then name parts, then tags like delete for trash. Separate several searches with commas to resolve many names in one call, like \"delete, settings, chevron down\".",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ query: { type: "string", description: "One search, or several separated by commas." },
32
+ limit: { type: "integer", minimum: 1, maximum: 50, description: "Results per search. Default 5." },
33
+ },
34
+ required: ["query"],
35
+ },
36
+ annotations: readOnly,
37
+ },
38
+ {
39
+ name: "get_icon",
40
+ title: "Get coolicons SVG",
41
+ description: "The SVG source of one icon in the chosen style and corner radius. Paints with currentColor.",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ name: { type: "string", description: "Exact icon name from search_icons, like trash." },
46
+ style,
47
+ radius,
48
+ size: { type: "integer", minimum: 1, description: "Width and height in pixels. Default 24." },
49
+ strokeWidth: { type: "number", minimum: 0.5, maximum: 4, description: "Stroke width for line, duocolor and duotone. Default 2." },
50
+ },
51
+ required: ["name"],
52
+ },
53
+ annotations: readOnly,
54
+ },
55
+ {
56
+ name: "get_usage",
57
+ title: "Get coolicons import",
58
+ description:
59
+ "Install command and import code for one or more icons in React, Vue, plain SVG or the shadcn/ui registry, using the entry for the chosen style and corner radius.",
60
+ inputSchema: {
61
+ type: "object",
62
+ properties: {
63
+ names: { type: "string", description: "Exact icon names, separated by commas." },
64
+ framework: { type: "string", enum: ["react", "vue", "svg", "shadcn"], description: "Default react." },
65
+ style,
66
+ radius,
67
+ },
68
+ required: ["names"],
69
+ },
70
+ annotations: readOnly,
71
+ },
72
+ ];
73
+
74
+ const list = (value) =>
75
+ String(value ?? "")
76
+ .split(",")
77
+ .map((part) => part.trim())
78
+ .filter(Boolean);
79
+
80
+ function describeSet() {
81
+ const { icons, clashes } = iconSet();
82
+ const counts = Object.entries(Object.groupBy(icons, (icon) => icon.category)).map(([category, members]) => `${category} (${members.length})`);
83
+ return `coolicons ${version}: ${icons.length} icons, each in ${STYLES.length} styles and ${RADII.length} corner radii, on a 24 pixel grid with a 2 pixel stroke. MIT license. ${SITE}
84
+
85
+ Styles: line (strokes), duocolor (strokes with a lighter second stroke), duotone (strokes over a light tone), fill (solid shapes), bulk (solid shapes over a light tone). Only ${[...STROKED].join(", ")} take a stroke width.
86
+ Radii: sharp, regular (default), round.
87
+
88
+ Packages: @coolicons/react and @coolicons/vue (one component per icon, entries like @coolicons/react/fill and @coolicons/react/bulk/round), @coolicons/svg (files at @coolicons/svg/<style>/<radius>/<name>.svg), and the shadcn/ui registry at ${SITE}/r/<name>.json.
89
+
90
+ Names are lowercase words joined by hyphens, like file-plus-corner. Components use PascalCase, like FilePlusCorner, and also export an Icon suffix. Use the suffix for ${Object.keys(clashes).length} names that clash with globals: ${Object.keys(clashes)
91
+ .map((name) => component({ name }))
92
+ .join(", ")}.
93
+
94
+ Categories: ${counts.join(", ")}.`;
95
+ }
96
+
97
+ function searchIcons({ query, limit = 5 }) {
98
+ const searches = list(query);
99
+ if (!searches.length) throw new LookupError("query is empty.");
100
+ const blocks = searches.map((term) => {
101
+ const found = search(term, Math.min(Math.max(Number(limit) || 5, 1), 50));
102
+ if (!found.length) {
103
+ const near = suggest(term);
104
+ return `"${term}": no match.${near.length ? ` Close names: ${near.join(", ")}.` : ""}`;
105
+ }
106
+ const rows = found.map((icon) => {
107
+ const alias = icon.aliasOf ? `, same drawing as ${icon.aliasOf}` : "";
108
+ return `- ${icon.name} (${component(icon)}, ${icon.category}${alias})${icon.keywords.length ? `: ${icon.keywords.join(", ")}` : ""}`;
109
+ });
110
+ return `"${term}":\n${rows.join("\n")}`;
111
+ });
112
+ return blocks.join("\n\n");
113
+ }
114
+
115
+ function getIcon({ name, style: chosen = "line", radius: corners = "regular", size = 24, strokeWidth = 2 }) {
116
+ if (!name) throw new LookupError("name is required.");
117
+ return svg(name, { style: chosen, radius: corners, size, strokeWidth });
118
+ }
119
+
120
+ function getUsage({ names, framework = "react", style: chosen = "line", radius: corners = "regular" }) {
121
+ const wanted = list(names);
122
+ if (!wanted.length) throw new LookupError("names is empty.");
123
+ const { install, code } = usage(wanted, { framework, style: chosen, radius: corners });
124
+ const clashing = wanted.map(findIcon).filter((icon) => Object.hasOwn(iconSet().clashes, icon.name));
125
+ const note = clashing.length && (framework === "react" || framework === "vue") ? `\n\nUse the Icon suffix for ${clashing.map(component).join(", ")}: the plain name is a JavaScript, browser or framework name.` : "";
126
+ return `${install}\n\n${code}${note}`;
127
+ }
128
+
129
+ const HANDLERS = { describe_set: describeSet, search_icons: searchIcons, get_icon: getIcon, get_usage: getUsage };
130
+
131
+ const send = (message) => process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
132
+ const failure = (id, code, message) => send({ id, error: { code, message } });
133
+
134
+ function handle(message) {
135
+ const { id, method, params = {} } = message;
136
+ const request = id !== undefined && id !== null;
137
+ if (!request) return;
138
+ if (method === "initialize") {
139
+ const asked = params.protocolVersion;
140
+ return send({
141
+ id,
142
+ result: {
143
+ protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
144
+ capabilities: { tools: { listChanged: false } },
145
+ serverInfo: { name: "coolicons", title: "coolicons", version },
146
+ instructions: INSTRUCTIONS,
147
+ },
148
+ });
149
+ }
150
+ if (method === "ping") return send({ id, result: {} });
151
+ if (method === "tools/list") return send({ id, result: { tools: TOOLS } });
152
+ if (method === "tools/call") {
153
+ const handler = HANDLERS[params.name];
154
+ if (!handler) return failure(id, -32602, `Unknown tool ${params.name}. Tools: ${Object.keys(HANDLERS).join(", ")}.`);
155
+ try {
156
+ return send({ id, result: { content: [{ type: "text", text: handler(params.arguments ?? {}) }] } });
157
+ } catch (error) {
158
+ if (!(error instanceof LookupError)) throw error;
159
+ return send({ id, result: { content: [{ type: "text", text: error.message }], isError: true } });
160
+ }
161
+ }
162
+ return failure(id, -32601, `Method not found: ${method}`);
163
+ }
164
+
165
+ const lines = createInterface({ input: process.stdin });
166
+ lines.on("line", (line) => {
167
+ if (!line.trim()) return;
168
+ let message;
169
+ try {
170
+ message = JSON.parse(line);
171
+ } catch {
172
+ return failure(null, -32700, "Parse error");
173
+ }
174
+ if (Array.isArray(message) || typeof message !== "object" || message === null) return failure(null, -32600, "Invalid request");
175
+ try {
176
+ handle(message);
177
+ } catch (error) {
178
+ process.stderr.write(`${error.stack ?? error}\n`);
179
+ if (message.id !== undefined && message.id !== null) failure(message.id, -32603, "Internal error");
180
+ }
181
+ });
package/package.json CHANGED
@@ -1,8 +1,18 @@
1
1
  {
2
2
  "name": "@coolicons/mcp",
3
- "version": "0.0.0",
4
- "description": "Reserved for coolicons 5, the first real version follows shortly",
3
+ "version": "5.0.0-beta.2",
4
+ "mcpName": "io.github.krystonschwarze/coolicons",
5
+ "description": "MCP server for coolicons: search the set, get SVG source and the right import, without guessing names",
6
+ "keywords": [
7
+ "coolicons",
8
+ "icons",
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "ai",
12
+ "agents"
13
+ ],
5
14
  "homepage": "https://coolicons.cool",
15
+ "bugs": "https://github.com/krystonschwarze/coolicons/issues",
6
16
  "repository": {
7
17
  "type": "git",
8
18
  "url": "git+https://github.com/krystonschwarze/coolicons.git",
@@ -12,5 +22,15 @@
12
22
  "author": {
13
23
  "name": "Kryston Schwarze",
14
24
  "url": "https://krystonschwarze.com"
25
+ },
26
+ "type": "module",
27
+ "bin": {
28
+ "coolicons-mcp": "./dist/server.js"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "engines": {
34
+ "node": ">=22"
15
35
  }
16
36
  }