@supericons/mcp 0.4.8 → 0.4.10

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/search.js CHANGED
@@ -38,6 +38,8 @@ const multilingualSearchAliases = existsSync(multilingualAliasesPath)
38
38
  : [];
39
39
  const multilingualExpansionTerms = [...cjkSearchTerms, ...multilingualSearchAliases];
40
40
  const iconSearchMetadataCache = new WeakMap();
41
+ const LOGO_INTENT_TOKENS = new Set(['logo', 'logos', 'icon', 'icons', 'brand', 'brands', 'mark', 'marks', 'symbol', 'symbols']);
42
+ const GENERIC_AI_LOGO_TOKENS = new Set(['ai', 'artificial', 'intelligence']);
41
43
 
42
44
  /** Inline Levenshtein distance (capped early for performance) */
43
45
  function editDistance(a, b) {
@@ -78,6 +80,60 @@ function getIconSemanticAliases(icon) {
78
80
  return iconSemanticAliasMap.get(iconKey(icon)) || null;
79
81
  }
80
82
 
83
+ function collectIconSearchValues(icon) {
84
+ const values = [
85
+ icon.name,
86
+ icon.id,
87
+ iconKey(icon),
88
+ icon.meaning,
89
+ icon.jobCategory,
90
+ icon.aiCategory,
91
+ icon.aiCategoryLabel,
92
+ icon.assetType,
93
+ icon.pack,
94
+ icon.access,
95
+ ];
96
+
97
+ for (const field of [
98
+ 'semanticTags',
99
+ 'synonyms',
100
+ 'aliases',
101
+ 'searchTerms',
102
+ 'filterTags',
103
+ 'aiFilterTags',
104
+ 'secondaryCategories',
105
+ 'variants',
106
+ ]) {
107
+ if (Array.isArray(icon[field])) {
108
+ values.push(...icon[field]);
109
+ }
110
+ }
111
+
112
+ return values.filter((value) => typeof value === 'string' && value.trim());
113
+ }
114
+
115
+ function getMeaningfulQueryWords(queryWords) {
116
+ const withoutLogoIntent = queryWords.filter((word) => !LOGO_INTENT_TOKENS.has(word));
117
+ const candidateWords = withoutLogoIntent.length > 0 ? withoutLogoIntent : queryWords;
118
+ const withoutGenericAi = candidateWords.length > 1
119
+ ? candidateWords.filter((word) => !GENERIC_AI_LOGO_TOKENS.has(word))
120
+ : candidateWords;
121
+ return withoutGenericAi.length > 0 ? withoutGenericAi : candidateWords;
122
+ }
123
+
124
+ function hasLogoIntent(queryWords) {
125
+ return queryWords.some((word) => LOGO_INTENT_TOKENS.has(word));
126
+ }
127
+
128
+ function isSupericonsBrandLogo(icon) {
129
+ return icon.lib === 'si'
130
+ && (
131
+ icon.assetType === 'brand-logo'
132
+ || icon.aiFilterTags?.includes('brand-logo')
133
+ || icon.filterTags?.includes('brand-logo')
134
+ );
135
+ }
136
+
81
137
  function getIconSearchMetadata(icon) {
82
138
  const cached = iconSearchMetadataCache.get(icon);
83
139
  if (cached) return cached;
@@ -91,9 +147,10 @@ function getIconSearchMetadata(icon) {
91
147
  ...tokenizeSemanticText(icon.name),
92
148
  ...tokenizeSemanticText(icon.id),
93
149
  ...tokenizeSemanticText(iconKey(icon)),
150
+ ...collectIconSearchValues(icon).flatMap((value) => tokenizeSemanticText(value)),
94
151
  ]);
95
152
  const segments = lowerId.split(/[-_]/).concat(lowerName.split(/[\s\-_]/));
96
- const aliases = (getIconSemanticAliases(icon) || [])
153
+ const aliases = [...(getIconSemanticAliases(icon) || []), ...collectIconSearchValues(icon)]
97
154
  .map((alias) => {
98
155
  const normalized = normalizeSemanticText(alias);
99
156
  return normalized
@@ -120,10 +177,11 @@ function getDirectSearchScore(icon, normalizedQuery, queryWords) {
120
177
  if (!normalizedQuery) return 0;
121
178
 
122
179
  const { name, id, fullId, tokens } = getIconSearchMetadata(icon);
123
-
124
- if (name === normalizedQuery || id === normalizedQuery || fullId === normalizedQuery) {
125
- return 320;
126
- }
180
+ const meaningfulQueryWords = getMeaningfulQueryWords(queryWords);
181
+
182
+ if (name === normalizedQuery || id === normalizedQuery || fullId === normalizedQuery) {
183
+ return 320;
184
+ }
127
185
 
128
186
  if (normalizedQuery.length > 2 && (
129
187
  name.includes(normalizedQuery)
@@ -133,14 +191,14 @@ function getDirectSearchScore(icon, normalizedQuery, queryWords) {
133
191
  return 250;
134
192
  }
135
193
 
136
- if (queryWords.length > 0 && queryWords.every((word) => tokens.has(word))) {
137
- return 190;
138
- }
139
-
140
- if (queryWords.length > 0 && queryWords.every((word) => (
141
- name.includes(word) || id.includes(word) || fullId.includes(word)
142
- ))) {
143
- return 150;
194
+ if (meaningfulQueryWords.length > 0 && meaningfulQueryWords.every((word) => tokens.has(word))) {
195
+ return 190;
196
+ }
197
+
198
+ if (meaningfulQueryWords.length > 0 && meaningfulQueryWords.every((word) => (
199
+ name.includes(word) || id.includes(word) || fullId.includes(word)
200
+ ))) {
201
+ return 150;
144
202
  }
145
203
 
146
204
  return 0;
@@ -153,6 +211,9 @@ function getCuratedAliasScore(icon, normalizedQuery, queryWords) {
153
211
  if (!aliases?.length) return 0;
154
212
 
155
213
  let bestScore = 0;
214
+ const meaningfulQueryWords = getMeaningfulQueryWords(queryWords);
215
+ const meaningfulQuery = meaningfulQueryWords.join(' ');
216
+ const shouldBoostBrandLogo = hasLogoIntent(queryWords) && isSupericonsBrandLogo(icon);
156
217
 
157
218
  for (const alias of aliases) {
158
219
  const { normalized: normalizedAlias, tokens: aliasTokens } = alias;
@@ -163,25 +224,30 @@ function getCuratedAliasScore(icon, normalizedQuery, queryWords) {
163
224
  continue;
164
225
  }
165
226
 
166
- if (normalizedQuery.length > 3 && normalizedAlias.includes(normalizedQuery)) {
167
- bestScore = Math.max(bestScore, 360);
168
- continue;
169
- }
170
-
171
- if (queryWords.length > 1 && queryWords.every((word) => aliasTokens.has(word))) {
172
- bestScore = Math.max(bestScore, 320);
173
- continue;
174
- }
175
-
176
- if (queryWords.length === 1 && aliasTokens.has(queryWords[0])) {
177
- bestScore = Math.max(bestScore, 260);
178
- continue;
179
- }
180
-
181
- if (queryWords.length > 0 && queryWords.every((word) => normalizedAlias.includes(word))) {
182
- bestScore = Math.max(bestScore, 220);
183
- }
184
- }
227
+ if (normalizedQuery.length > 3 && normalizedAlias.includes(normalizedQuery)) {
228
+ bestScore = Math.max(bestScore, 360);
229
+ continue;
230
+ }
231
+
232
+ if (meaningfulQuery.length > 3 && normalizedAlias.includes(meaningfulQuery)) {
233
+ bestScore = Math.max(bestScore, shouldBoostBrandLogo ? 520 : 350);
234
+ continue;
235
+ }
236
+
237
+ if (meaningfulQueryWords.length > 1 && meaningfulQueryWords.every((word) => aliasTokens.has(word))) {
238
+ bestScore = Math.max(bestScore, shouldBoostBrandLogo ? 490 : 320);
239
+ continue;
240
+ }
241
+
242
+ if (meaningfulQueryWords.length === 1 && aliasTokens.has(meaningfulQueryWords[0])) {
243
+ bestScore = Math.max(bestScore, shouldBoostBrandLogo ? 460 : 260);
244
+ continue;
245
+ }
246
+
247
+ if (meaningfulQueryWords.length > 0 && meaningfulQueryWords.every((word) => normalizedAlias.includes(word))) {
248
+ bestScore = Math.max(bestScore, shouldBoostBrandLogo ? 420 : 220);
249
+ }
250
+ }
185
251
 
186
252
  return bestScore;
187
253
  }
@@ -2,15 +2,40 @@ import { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { getBaseSemanticIdsForVariant } from './variant-support.js';
4
4
 
5
- const PUBLIC_SEMANTIC_FIELDS = Object.freeze([
6
- 'label',
7
- 'source_name',
8
- 'depicts',
9
- 'semantic_tags',
10
- 'synonyms',
11
- 'use_when',
12
- 'avoid_when',
13
- ]);
5
+ const PUBLIC_SEMANTIC_FIELDS = Object.freeze([
6
+ 'id',
7
+ 'source_library',
8
+ 'label',
9
+ 'name',
10
+ 'slug',
11
+ 'source_name',
12
+ 'purpose',
13
+ 'category',
14
+ 'asset_type',
15
+ 'pack',
16
+ 'source_url',
17
+ 'source_trust',
18
+ 'meaning',
19
+ 'depicts',
20
+ 'semantic_tags',
21
+ 'ai_category',
22
+ 'ai_category_label',
23
+ 'ai_filter_tags',
24
+ 'job_category',
25
+ 'secondary_categories',
26
+ 'synonyms',
27
+ 'aliases',
28
+ 'search_terms',
29
+ 'filter_tags',
30
+ 'use_when',
31
+ 'avoid_when',
32
+ 'rights',
33
+ 'variants',
34
+ 'quality_status',
35
+ 'access',
36
+ ]);
37
+ const LOGO_INTENT_TOKENS = new Set(['logo', 'logos', 'icon', 'icons', 'brand', 'brands', 'mark', 'marks', 'symbol', 'symbols']);
38
+ const GENERIC_AI_TOKENS = new Set(['ai', 'artificial', 'intelligence']);
14
39
 
15
40
  function normalizeText(value) {
16
41
  return String(value || '')
@@ -22,10 +47,95 @@ function normalizeText(value) {
22
47
  .trim();
23
48
  }
24
49
 
25
- function tokenize(value) {
26
- const normalized = normalizeText(value);
27
- return normalized ? normalized.split(' ') : [];
28
- }
50
+ function tokenize(value) {
51
+ const normalized = normalizeText(value);
52
+ return normalized ? normalized.split(' ') : [];
53
+ }
54
+
55
+ function uniqueStrings(values = []) {
56
+ const seen = new Set();
57
+ const output = [];
58
+
59
+ for (const value of values) {
60
+ if (typeof value !== 'string') continue;
61
+ const normalized = normalizeText(value);
62
+ if (!normalized || seen.has(normalized)) continue;
63
+ seen.add(normalized);
64
+ output.push(normalized);
65
+ }
66
+
67
+ return output;
68
+ }
69
+
70
+ function getMeaningfulQueryTokens(queryTokens) {
71
+ const withoutLogoIntent = queryTokens.filter((token) => !LOGO_INTENT_TOKENS.has(token));
72
+ const candidateTokens = withoutLogoIntent.length > 0 ? withoutLogoIntent : queryTokens;
73
+ const withoutGenericAi = candidateTokens.length > 1
74
+ ? candidateTokens.filter((token) => !GENERIC_AI_TOKENS.has(token))
75
+ : candidateTokens;
76
+
77
+ return withoutGenericAi.length > 0 ? withoutGenericAi : candidateTokens;
78
+ }
79
+
80
+ function buildQueryPhraseVariants(queryTokens, meaningfulTokens) {
81
+ const variants = [
82
+ queryTokens.join(' '),
83
+ meaningfulTokens.join(' '),
84
+ ];
85
+
86
+ for (let size = Math.min(4, meaningfulTokens.length); size >= 2; size -= 1) {
87
+ for (let index = 0; index <= meaningfulTokens.length - size; index += 1) {
88
+ variants.push(meaningfulTokens.slice(index, index + size).join(' '));
89
+ }
90
+ }
91
+
92
+ return uniqueStrings(variants).filter((variant) => variant.length > 2);
93
+ }
94
+
95
+ function scoreSemanticValue(value, weight, phraseVariants, meaningfulTokens) {
96
+ const normalized = normalizeText(value);
97
+ if (!normalized) return 0;
98
+
99
+ let score = 0;
100
+
101
+ for (const phrase of phraseVariants) {
102
+ if (normalized === phrase) {
103
+ score = Math.max(score, weight * 4);
104
+ continue;
105
+ }
106
+
107
+ if (phrase.length > 2 && normalized.includes(phrase)) {
108
+ score = Math.max(score, weight * 2.6);
109
+ continue;
110
+ }
111
+
112
+ if (normalized.length > 2 && phrase.includes(normalized) && tokenize(normalized).length > 1) {
113
+ score = Math.max(score, weight * 1.6);
114
+ }
115
+ }
116
+
117
+ if (meaningfulTokens.length > 0) {
118
+ const valueTokens = new Set(tokenize(normalized));
119
+ const exactHits = meaningfulTokens.filter((token) => valueTokens.has(token)).length;
120
+ const includesHits = meaningfulTokens.filter((token) => normalized.includes(token)).length;
121
+
122
+ if (exactHits === meaningfulTokens.length) {
123
+ score = Math.max(score, weight * 1.8);
124
+ } else if (includesHits > 0) {
125
+ score += includesHits * weight * 0.35;
126
+ }
127
+ }
128
+
129
+ return score;
130
+ }
131
+
132
+ function scoreSemanticValues(values, weight, phraseVariants, meaningfulTokens) {
133
+ if (!Array.isArray(values)) return 0;
134
+ return values.reduce(
135
+ (total, value) => total + scoreSemanticValue(value, weight, phraseVariants, meaningfulTokens),
136
+ 0,
137
+ );
138
+ }
29
139
 
30
140
  function buildPossibleRegistryIds(library, id) {
31
141
  return getBaseSemanticIdsForVariant({ library, id });
@@ -88,49 +198,60 @@ export function attachSemanticPayload(target, semanticMap, iconOrLibrary, maybeI
88
198
  return semantic ? { ...target, semantic } : target;
89
199
  }
90
200
 
91
- export function scoreSemanticAlignment(query, semanticRecord) {
92
- if (!semanticRecord) return 0;
93
-
94
- const queryTokens = tokenize(query);
95
- if (queryTokens.length === 0) return 0;
96
-
97
- const weightedSources = [
98
- { value: semanticRecord.label, weight: 6 },
99
- { value: semanticRecord.source_name, weight: 5 },
100
- { value: semanticRecord.depicts, weight: 4 },
101
- { value: semanticRecord.use_when, weight: 3 },
102
- { value: semanticRecord.avoid_when, weight: 2 },
103
- ];
104
-
105
- let score = 0;
106
-
107
- for (const tag of semanticRecord.semantic_tags || []) {
108
- const normalized = normalizeText(tag);
109
- for (const token of queryTokens) {
110
- if (normalized === token) score += 8;
111
- else if (normalized.includes(token)) score += 5;
112
- }
113
- }
114
-
115
- for (const synonym of semanticRecord.synonyms || []) {
116
- const normalized = normalizeText(synonym);
117
- for (const token of queryTokens) {
118
- if (normalized === token) score += 7;
119
- else if (normalized.includes(token)) score += 4;
120
- }
121
- }
122
-
123
- for (const source of weightedSources) {
124
- const normalized = normalizeText(source.value);
125
- if (!normalized) continue;
126
- for (const token of queryTokens) {
127
- if (normalized === token) score += source.weight + 2;
128
- else if (normalized.includes(token)) score += source.weight;
129
- }
130
- }
131
-
132
- return score;
133
- }
201
+ export function scoreSemanticAlignment(query, semanticRecord) {
202
+ if (!semanticRecord) return 0;
203
+
204
+ const queryTokens = tokenize(query);
205
+ if (queryTokens.length === 0) return 0;
206
+
207
+ const meaningfulTokens = getMeaningfulQueryTokens(queryTokens);
208
+ const phraseVariants = buildQueryPhraseVariants(queryTokens, meaningfulTokens);
209
+ const hasLogoIntent = queryTokens.some((token) => LOGO_INTENT_TOKENS.has(token));
210
+ const isSupericonsBrandLogo = semanticRecord.source_library === 'si'
211
+ && (
212
+ semanticRecord.asset_type === 'brand-logo'
213
+ || semanticRecord.ai_filter_tags?.includes('brand-logo')
214
+ || semanticRecord.filter_tags?.includes('brand-logo')
215
+ );
216
+ let score = 0;
217
+
218
+ score += scoreSemanticValue(semanticRecord.label, 24, phraseVariants, meaningfulTokens);
219
+ score += scoreSemanticValue(semanticRecord.name, 24, phraseVariants, meaningfulTokens);
220
+ score += scoreSemanticValue(semanticRecord.source_name, 22, phraseVariants, meaningfulTokens);
221
+ score += scoreSemanticValues(semanticRecord.aliases, 22, phraseVariants, meaningfulTokens);
222
+ score += scoreSemanticValues(semanticRecord.synonyms, 21, phraseVariants, meaningfulTokens);
223
+ score += scoreSemanticValues(semanticRecord.semantic_tags, 19, phraseVariants, meaningfulTokens);
224
+ score += scoreSemanticValues(semanticRecord.search_terms, 18, phraseVariants, meaningfulTokens);
225
+ score += scoreSemanticValue(semanticRecord.meaning, 16, phraseVariants, meaningfulTokens);
226
+ score += scoreSemanticValue(semanticRecord.purpose, 15, phraseVariants, meaningfulTokens);
227
+ score += scoreSemanticValues(semanticRecord.ai_filter_tags, 14, phraseVariants, meaningfulTokens);
228
+ score += scoreSemanticValues(semanticRecord.filter_tags, 13, phraseVariants, meaningfulTokens);
229
+ score += scoreSemanticValues(semanticRecord.secondary_categories, 12, phraseVariants, meaningfulTokens);
230
+ score += scoreSemanticValue(semanticRecord.ai_category_label, 10, phraseVariants, meaningfulTokens);
231
+ score += scoreSemanticValue(semanticRecord.job_category, 10, phraseVariants, meaningfulTokens);
232
+ score += scoreSemanticValue(semanticRecord.category, 8, phraseVariants, meaningfulTokens);
233
+ score += scoreSemanticValue(semanticRecord.depicts, 8, phraseVariants, meaningfulTokens);
234
+ score += scoreSemanticValue(semanticRecord.use_when, 8, phraseVariants, meaningfulTokens);
235
+ score += scoreSemanticValue(semanticRecord.avoid_when, 1, phraseVariants, meaningfulTokens);
236
+
237
+ if (hasLogoIntent && isSupericonsBrandLogo) {
238
+ score += 10;
239
+ }
240
+
241
+ const identityValues = uniqueStrings([
242
+ semanticRecord.label,
243
+ semanticRecord.name,
244
+ semanticRecord.source_name,
245
+ ...(semanticRecord.aliases || []),
246
+ ...(semanticRecord.synonyms || []),
247
+ ]);
248
+ const meaningfulQuery = meaningfulTokens.join(' ');
249
+ if (hasLogoIntent && identityValues.some((value) => value === meaningfulQuery)) {
250
+ score += 80;
251
+ }
252
+
253
+ return score;
254
+ }
134
255
 
135
256
  export function chooseSemanticCandidate(query, icons, semanticMap) {
136
257
  const scored = icons
package/server.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/curlymolelabs/supericons",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.4.7",
10
+ "version": "0.4.10",
11
11
  "remotes": [
12
12
  {
13
13
  "type": "streamable-http",
@@ -18,7 +18,7 @@
18
18
  {
19
19
  "registryType": "npm",
20
20
  "identifier": "@supericons/mcp",
21
- "version": "0.4.7",
21
+ "version": "0.4.10",
22
22
  "transport": {
23
23
  "type": "stdio"
24
24
  }