@supericons/mcp 0.4.6

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/server.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.curlymolelabs/supericons",
4
+ "title": "Supericons",
5
+ "description": "Multilingual semantic SVG icon search and recommendations for AI coding agents. Search 20,000+ curated icons by meaning, use case, UI slot, or library.",
6
+ "repository": {
7
+ "url": "https://github.com/curlymolelabs/supericons",
8
+ "source": "github"
9
+ },
10
+ "version": "0.4.4",
11
+ "remotes": [
12
+ {
13
+ "type": "streamable-http",
14
+ "url": "https://mcp.supericons.dev/mcp"
15
+ }
16
+ ],
17
+ "packages": [
18
+ {
19
+ "registryType": "npm",
20
+ "identifier": "supericons-mcp",
21
+ "version": "0.4.4",
22
+ "transport": {
23
+ "type": "stdio"
24
+ }
25
+ }
26
+ ]
27
+ }
package/telemetry.js ADDED
@@ -0,0 +1,85 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { SUPABASE_ANON, SUPABASE_URL } from './auth.js';
3
+
4
+ const PROCESS_SESSION_TOKEN = randomUUID();
5
+
6
+ function getSessionHash() {
7
+ const today = new Date().toISOString().slice(0, 10);
8
+ return createHash('sha256')
9
+ .update(`${PROCESS_SESSION_TOKEN}|${today}`)
10
+ .digest('hex');
11
+ }
12
+
13
+ async function callRpc(name, payload) {
14
+ const response = await fetch(`${SUPABASE_URL}/rest/v1/rpc/${name}`, {
15
+ method: 'POST',
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ apikey: SUPABASE_ANON,
19
+ Authorization: `Bearer ${SUPABASE_ANON}`,
20
+ Prefer: 'return=minimal',
21
+ },
22
+ body: JSON.stringify(payload),
23
+ });
24
+
25
+ if (!response.ok) {
26
+ throw new Error(`RPC ${name} failed (${response.status})`);
27
+ }
28
+ }
29
+
30
+ export async function logMcpSearchBatch({
31
+ query,
32
+ results,
33
+ locale = null,
34
+ }) {
35
+ if (!Array.isArray(results) || results.length === 0) return;
36
+
37
+ const sessionHash = getSessionHash();
38
+ const batchId = randomUUID();
39
+
40
+ try {
41
+ await callRpc('si_mark_superseded_mcp_batches', {
42
+ p_session_hash: sessionHash,
43
+ });
44
+
45
+ await Promise.allSettled(results.map((result, index) => callRpc('si_log_icon_evidence', {
46
+ p_signal_type: 'mcp_call',
47
+ p_icon_id: `${result.library}:${result.id}`,
48
+ p_batch_id: batchId,
49
+ p_search_query: query || null,
50
+ p_result_position: index + 1,
51
+ p_ui_surface: 'mcp',
52
+ p_evidence_text: locale ? `search_icons locale=${locale}` : 'search_icons',
53
+ p_session_hash: sessionHash,
54
+ p_created_at: new Date().toISOString(),
55
+ })));
56
+ } catch (error) {
57
+ console.error('[SuperIcons] MCP telemetry failed:', error.message);
58
+ }
59
+ }
60
+
61
+ export async function logMcpSearchAttempt({
62
+ query,
63
+ resultCount,
64
+ libraryFilter = null,
65
+ locale = null,
66
+ }) {
67
+ const normalizedQuery = String(query || '').trim().toLowerCase().replace(/\s+/g, ' ');
68
+ const safeResultCount = Number.isFinite(resultCount) ? Math.max(0, Math.round(resultCount)) : null;
69
+ if (!normalizedQuery || safeResultCount === null) return;
70
+
71
+ try {
72
+ await callRpc('si_log_icon_evidence', {
73
+ p_signal_type: 'search_attempt',
74
+ p_search_query: normalizedQuery,
75
+ p_result_count: safeResultCount,
76
+ p_library_filter: libraryFilter || 'all',
77
+ p_ui_surface: 'mcp',
78
+ p_evidence_text: locale ? `search_icons locale=${locale}` : 'search_icons',
79
+ p_session_hash: getSessionHash(),
80
+ p_created_at: new Date().toISOString(),
81
+ });
82
+ } catch (error) {
83
+ console.error('[SuperIcons] MCP search-attempt telemetry failed:', error.message);
84
+ }
85
+ }
@@ -0,0 +1,236 @@
1
+ export const VARIANT_STYLES = Object.freeze({
2
+ ANY: 'any',
3
+ OUTLINE: 'outline',
4
+ SOLID: 'solid',
5
+ });
6
+
7
+ const DEFAULT_STRATEGY = Object.freeze({
8
+ kind: 'none',
9
+ supportsSolid: false,
10
+ });
11
+
12
+ const VARIANT_STRATEGIES = Object.freeze({
13
+ material: Object.freeze({
14
+ kind: 'material-fill-axis',
15
+ supportsSolid: true,
16
+ }),
17
+ tabler: Object.freeze({
18
+ kind: 'same-id-style-pair',
19
+ supportsSolid: true,
20
+ assetSeparator: 'hyphen',
21
+ }),
22
+ phosphor: Object.freeze({
23
+ kind: 'fill-suffix-pair',
24
+ supportsSolid: true,
25
+ solidSuffix: '-fill',
26
+ assetSeparator: 'hyphen',
27
+ }),
28
+ heroicons: Object.freeze({
29
+ kind: 'same-id-style-pair',
30
+ supportsSolid: true,
31
+ assetSeparator: 'hyphen',
32
+ }),
33
+ bootstrap: Object.freeze({
34
+ kind: 'fill-suffix-pair',
35
+ supportsSolid: true,
36
+ solidSuffix: '-fill',
37
+ assetSeparator: 'hyphen',
38
+ }),
39
+ ionicons: Object.freeze({
40
+ kind: 'outline-suffix-pair',
41
+ supportsSolid: true,
42
+ outlineSuffix: '-outline',
43
+ assetSeparator: 'hyphen',
44
+ }),
45
+ iconoir: Object.freeze({
46
+ kind: 'same-id-style-pair',
47
+ supportsSolid: true,
48
+ assetSeparator: 'hyphen',
49
+ }),
50
+ mingcute: Object.freeze({
51
+ kind: 'line-fill-suffix-pair',
52
+ supportsSolid: true,
53
+ outlineSuffix: '_line',
54
+ solidSuffix: '_fill',
55
+ }),
56
+ });
57
+
58
+ export const MCP_VARIANT_CAPABLE_LIBRARIES = Object.freeze(
59
+ Object.fromEntries(
60
+ Object.entries(VARIANT_STRATEGIES)
61
+ .filter(([, strategy]) => strategy.supportsSolid)
62
+ .map(([library, strategy]) => [library, strategy])
63
+ )
64
+ );
65
+
66
+ function dedupe(values) {
67
+ return [...new Set(values.filter(Boolean))];
68
+ }
69
+
70
+ export function normalizeRequestedStyle(style) {
71
+ const normalized = String(style || VARIANT_STYLES.ANY).trim().toLowerCase();
72
+ if (normalized === VARIANT_STYLES.OUTLINE || normalized === VARIANT_STYLES.SOLID) {
73
+ return normalized;
74
+ }
75
+ return VARIANT_STYLES.ANY;
76
+ }
77
+
78
+ export function getVariantStrategyForLibrary(library) {
79
+ return VARIANT_STRATEGIES[library] || DEFAULT_STRATEGY;
80
+ }
81
+
82
+ export function librarySupportsSolid(library) {
83
+ return Boolean(getVariantStrategyForLibrary(library).supportsSolid);
84
+ }
85
+
86
+ export function getVariantConceptId(library, id) {
87
+ const normalizedId = String(id || '').trim();
88
+ const strategy = getVariantStrategyForLibrary(library);
89
+
90
+ if (!normalizedId) return normalizedId;
91
+
92
+ if (strategy.kind === 'fill-suffix-pair') {
93
+ return normalizedId.replace(new RegExp(`${strategy.solidSuffix}$`, 'i'), '');
94
+ }
95
+
96
+ if (strategy.kind === 'outline-suffix-pair') {
97
+ return normalizedId.replace(new RegExp(`${strategy.outlineSuffix}$`, 'i'), '');
98
+ }
99
+
100
+ if (strategy.kind === 'line-fill-suffix-pair') {
101
+ return normalizedId
102
+ .replace(new RegExp(`${strategy.outlineSuffix}$`, 'i'), '')
103
+ .replace(new RegExp(`${strategy.solidSuffix}$`, 'i'), '');
104
+ }
105
+
106
+ return normalizedId;
107
+ }
108
+
109
+ export function buildVariantLookupCandidates({ library, id, style = VARIANT_STYLES.ANY }) {
110
+ const normalizedStyle = normalizeRequestedStyle(style);
111
+ const rawId = String(id || '').trim();
112
+ const strategy = getVariantStrategyForLibrary(library);
113
+ const normalizedId = strategy.assetSeparator === 'hyphen' ? rawId.replace(/_/g, '-') : rawId;
114
+
115
+ if (!normalizedId) return [];
116
+
117
+ const candidates = [normalizedId];
118
+
119
+ if (strategy.kind === 'fill-suffix-pair') {
120
+ const baseId = getVariantConceptId(library, normalizedId);
121
+ const solidId = `${baseId}${strategy.solidSuffix}`;
122
+
123
+ if (normalizedStyle === VARIANT_STYLES.OUTLINE) {
124
+ candidates.push(baseId);
125
+ } else if (normalizedStyle === VARIANT_STYLES.SOLID) {
126
+ candidates.push(solidId);
127
+ } else if (!normalizedId.endsWith(strategy.solidSuffix)) {
128
+ candidates.push(solidId);
129
+ } else {
130
+ candidates.push(baseId);
131
+ }
132
+ }
133
+
134
+ if (strategy.kind === 'outline-suffix-pair') {
135
+ const baseId = getVariantConceptId(library, normalizedId);
136
+ const outlineId = `${baseId}${strategy.outlineSuffix}`;
137
+
138
+ if (normalizedStyle === VARIANT_STYLES.OUTLINE) {
139
+ candidates.push(outlineId);
140
+ } else if (normalizedStyle === VARIANT_STYLES.SOLID) {
141
+ candidates.push(baseId);
142
+ } else if (!normalizedId.endsWith(strategy.outlineSuffix)) {
143
+ candidates.push(outlineId);
144
+ } else {
145
+ candidates.push(baseId);
146
+ }
147
+ }
148
+
149
+ if (strategy.kind === 'line-fill-suffix-pair') {
150
+ const baseId = getVariantConceptId(library, normalizedId);
151
+ const outlineId = `${baseId}${strategy.outlineSuffix}`;
152
+ const solidId = `${baseId}${strategy.solidSuffix}`;
153
+
154
+ if (normalizedStyle === VARIANT_STYLES.OUTLINE) {
155
+ candidates.push(outlineId);
156
+ } else if (normalizedStyle === VARIANT_STYLES.SOLID) {
157
+ candidates.push(solidId);
158
+ } else if (normalizedId.endsWith(strategy.outlineSuffix)) {
159
+ candidates.push(solidId);
160
+ } else if (normalizedId.endsWith(strategy.solidSuffix)) {
161
+ candidates.push(outlineId);
162
+ } else {
163
+ candidates.push(outlineId, solidId);
164
+ }
165
+ }
166
+
167
+ return dedupe(candidates);
168
+ }
169
+
170
+ export function getBaseSemanticIdsForVariant({ library, id }) {
171
+ const normalizedId = String(id || '').trim();
172
+ const conceptId = getVariantConceptId(library, normalizedId);
173
+ const strategy = getVariantStrategyForLibrary(library);
174
+ const normalizedIdUnderscore = normalizedId.replace(/-/g, '_');
175
+ const conceptIdUnderscore = conceptId.replace(/-/g, '_');
176
+
177
+ const ids = [`${library}:${normalizedId}`];
178
+
179
+ if (conceptId && conceptId !== normalizedId) {
180
+ ids.push(`${library}:${conceptId}`);
181
+ }
182
+
183
+ if (normalizedIdUnderscore && normalizedIdUnderscore !== normalizedId) {
184
+ ids.push(`${library}:${normalizedIdUnderscore}`);
185
+ }
186
+
187
+ if (conceptIdUnderscore && conceptIdUnderscore !== conceptId) {
188
+ ids.push(`${library}:${conceptIdUnderscore}`);
189
+ }
190
+
191
+ if (strategy.kind === 'outline-suffix-pair' && conceptId) {
192
+ ids.push(`${library}:${conceptId}${strategy.outlineSuffix}`);
193
+ ids.push(`${library}:${conceptIdUnderscore}${strategy.outlineSuffix.replace(/-/g, '_')}`);
194
+ }
195
+
196
+ return dedupe(ids);
197
+ }
198
+
199
+ export function iconMatchesRequestedStyle(icon, requestedStyle) {
200
+ const normalizedStyle = normalizeRequestedStyle(requestedStyle);
201
+ if (normalizedStyle === VARIANT_STYLES.ANY) return true;
202
+
203
+ if (icon?.lib === 'material') {
204
+ return true;
205
+ }
206
+
207
+ return icon?.style === normalizedStyle;
208
+ }
209
+
210
+ export function getConceptKeyForIcon(icon) {
211
+ if (!icon) return null;
212
+ const library = icon.lib || icon.library;
213
+ const id = icon.id || icon.source_name;
214
+ if (!library || !id) return null;
215
+ return `${library}:${getVariantConceptId(library, id)}`;
216
+ }
217
+
218
+ export function compareVariantPreference(left, right, requestedStyle = VARIANT_STYLES.ANY) {
219
+ const style = normalizeRequestedStyle(requestedStyle);
220
+
221
+ if (style === VARIANT_STYLES.OUTLINE) {
222
+ const leftScore = left?.style === VARIANT_STYLES.OUTLINE || left?.lib === 'material' ? 1 : 0;
223
+ const rightScore = right?.style === VARIANT_STYLES.OUTLINE || right?.lib === 'material' ? 1 : 0;
224
+ return rightScore - leftScore;
225
+ }
226
+
227
+ if (style === VARIANT_STYLES.SOLID) {
228
+ const leftScore = left?.style === VARIANT_STYLES.SOLID || left?.lib === 'material' ? 1 : 0;
229
+ const rightScore = right?.style === VARIANT_STYLES.SOLID || right?.lib === 'material' ? 1 : 0;
230
+ return rightScore - leftScore;
231
+ }
232
+
233
+ const leftScore = left?.style === VARIANT_STYLES.OUTLINE || left?.lib === 'material' ? 1 : 0;
234
+ const rightScore = right?.style === VARIANT_STYLES.OUTLINE || right?.lib === 'material' ? 1 : 0;
235
+ return rightScore - leftScore;
236
+ }
@@ -0,0 +1,65 @@
1
+ import { getConfiguredApiKey } from './auth.js';
2
+
3
+ export function hasPremiumLibraryAccess(authState, library) {
4
+ if (authState?.isPro) return true;
5
+ return Array.isArray(authState?.purchasedSlugs) && authState.purchasedSlugs.includes(library);
6
+ }
7
+
8
+ export function hasProWorkflowAccess(authState) {
9
+ return Boolean(authState?.isPro);
10
+ }
11
+
12
+ export function buildPremiumLibraryAccessError(
13
+ libraryName,
14
+ hint = 'Set SUPERICONS_API_KEY in your MCP config with an API key that includes access to this premium pack.'
15
+ ) {
16
+ return {
17
+ error: 'Premium access required',
18
+ code: 'premium_library_access_required',
19
+ message: `The "${libraryName}" pack requires a purchase or Pro subscription. Visit https://supericons.dev`,
20
+ hint,
21
+ retryable: false,
22
+ };
23
+ }
24
+
25
+ export function buildProWorkflowAccessError(authState, featureName) {
26
+ const configuredApiKey = getConfiguredApiKey();
27
+
28
+ if (!configuredApiKey) {
29
+ return {
30
+ error: 'API key required',
31
+ code: 'workflow_api_key_required',
32
+ message: `${featureName} requires a Pro-linked SUPERICONS_API_KEY.`,
33
+ hint: 'Add SUPERICONS_API_KEY to your MCP config, then restart the MCP client.',
34
+ retryable: false,
35
+ };
36
+ }
37
+
38
+ if (authState?.authenticated && !authState?.isPro) {
39
+ return {
40
+ error: 'Pro workflow access required',
41
+ code: 'workflow_pro_required',
42
+ message: `${featureName} is available through Supericons Pro workflow access. Your current API key is valid, but it is not linked to a Pro subscription.`,
43
+ hint: 'Upgrade the account linked to this API key to Pro, or switch to a different Pro-linked SUPERICONS_API_KEY.',
44
+ retryable: false,
45
+ };
46
+ }
47
+
48
+ if (authState?.error) {
49
+ return {
50
+ error: 'Workflow access validation failed',
51
+ code: 'workflow_access_validation_failed',
52
+ message: `${featureName} could not confirm your workflow access right now.`,
53
+ hint: `Current auth validation error: ${authState.error}`,
54
+ retryable: true,
55
+ };
56
+ }
57
+
58
+ return {
59
+ error: 'Pro workflow access required',
60
+ code: 'workflow_pro_required',
61
+ message: `${featureName} is available through Supericons Pro workflow access. Visit https://supericons.dev/pricing and connect a Pro-linked SUPERICONS_API_KEY.`,
62
+ hint: 'Generate or manage your API key from Dashboard > API Keys after subscribing to Pro.',
63
+ retryable: false,
64
+ };
65
+ }