@supericons/mcp
Advanced tools
+13
-0
| # Changelog | ||
| ## 0.4.27 - 2026-08-03 | ||
| ### Fixed | ||
| - added clear cross-library guidance after a valid strict-library zero while keeping strict results empty | ||
| - protected exact brand intent from generic icon meanings, including Copy.ai punctuation forms | ||
| - distinguished reviewed `.ai` brand names from Illustrator and other `.ai` file searches | ||
| ### Improved | ||
| - clarified when agents should use all, preferred, or strict library search | ||
| - added permanent cross-surface checks for library recovery, brand safety, `.ai` files, and hosted error visibility | ||
| ## 0.4.26 - 2026-07-30 | ||
@@ -4,0 +17,0 @@ |
| { | ||
| "version": 1, | ||
| "generated_at": "2026-07-29T21:02:52.420Z", | ||
| "generated_at": "2026-08-03T13:27:43.760Z", | ||
| "presets": [ | ||
@@ -5,0 +5,0 @@ { |
+1
-1
| { | ||
| "name": "@supericons/mcp", | ||
| "version": "0.4.26", | ||
| "version": "0.4.27", | ||
| "mcpName": "io.github.curlymolelabs/supericons", | ||
@@ -5,0 +5,0 @@ "description": "MCP server for Supericons: multilingual semantic SVG icon search and recommendations for AI coding agents.", |
+20
-0
@@ -193,2 +193,21 @@ import { Resvg } from '@resvg/resvg-js'; | ||
| : renderedCount; | ||
| const iconRefs = icons | ||
| .map((icon) => icon.icon_ref || `${icon.library || icon.lib}:${icon.id}`) | ||
| .filter(Boolean); | ||
| const suggestedLines = renderedCount > 0 | ||
| ? [ | ||
| `Previewed ${renderedCount} verified icon${renderedCount === 1 ? '' : 's'}:`, | ||
| ...(markdownImage ? ['', markdownImage] : []), | ||
| '', | ||
| ...iconRefs.map((ref) => `- \`${ref}\``), | ||
| '', | ||
| `[Open the visual preview](${previewUrl})`, | ||
| '', | ||
| 'If this client cannot display the image inline, use the visual preview link above.', | ||
| ] | ||
| : [ | ||
| 'No icons were available for this preview.', | ||
| '', | ||
| `[Open the visual preview](${previewUrl})`, | ||
| ]; | ||
| return { | ||
@@ -209,2 +228,3 @@ query, | ||
| : 'Try a broader query or provide different icon refs.', | ||
| suggested_response_markdown: suggestedLines.join('\n'), | ||
| client_display_note: imageIncluded | ||
@@ -211,0 +231,0 @@ ? 'If your MCP client does not render image content inline, use markdown_image in the final answer or open image_url/preview_url in a browser.' |
| { | ||
| "generatedAt": "2026-07-29T21:02:44.779Z", | ||
| "freeIconCount": 21467, | ||
| "generatedAt": "2026-08-03T13:27:35.971Z", | ||
| "freeIconCount": 21468, | ||
| "freeLibraryCount": 11, | ||
@@ -9,3 +9,3 @@ "premiumCollectionCount": 9, | ||
| "mcpFreeToolCount": 5, | ||
| "mcpPackageVersion": "0.4.26", | ||
| "mcpPackageVersion": "0.4.27", | ||
| "display": { | ||
@@ -12,0 +12,0 @@ "freeIconsRounded": "20,000+", |
@@ -215,3 +215,3 @@ const DEFAULT_LOCAL_FIRST_VALUE = 'on'; | ||
| return validHostedResults.slice(0, Math.max(1, Number(params.limit) || 20)); | ||
| return mergeResults(params, [], validHostedResults); | ||
| } | ||
@@ -218,0 +218,0 @@ |
@@ -11,2 +11,3 @@ /** | ||
| getSearchInterpretationPlan, | ||
| hasDomainStyleBrandIntent, | ||
| normalizeSearchLibraryMode, | ||
@@ -641,2 +642,27 @@ rerankSearchCandidatesAtFusion, | ||
| const libraryMode = normalizeSearchLibraryMode(options.libraryMode); | ||
| if (hasDomainStyleBrandIntent(query)) { | ||
| const directBrandResults = searchIconsForSingleQuery(query, icons, synonyms, { | ||
| library, | ||
| libraryMode, | ||
| limit: Math.max(Number(limit || 20) * 2, 20), | ||
| style, | ||
| applyExpressiveFallback: false, | ||
| candidatePool: getIndexedCandidatePool(icons, query, synonyms), | ||
| }).filter((icon) => ( | ||
| isLikelyBrandIdentityIcon(icon) | ||
| && getBrandRankAdjustment(query, icon).penalty === 0 | ||
| )); | ||
| const exactBrandResults = directBrandResults.length > 0 | ||
| ? directBrandResults | ||
| : getExactBrandIdentityResults(query, icons, synonyms, { | ||
| library, | ||
| libraryMode, | ||
| limit, | ||
| style, | ||
| }); | ||
| return rerankSearchCandidatesAtFusion(query, exactBrandResults, { | ||
| libraryMode, | ||
| requestedLibrary: library, | ||
| }).slice(0, Math.max(1, limit)); | ||
| } | ||
| const semanticQueryFrame = buildSearchQueryFrame(query); | ||
@@ -643,0 +669,0 @@ const cjkExpansion = expandCjkQuery(query, { |
@@ -10,2 +10,19 @@ import { GENERATED_SEARCH_RANKING_POLICY } from './generated-search-ranking-policy.js'; | ||
| const expressiveBroadMatchPenalty = Number(policy.candidate_strength_policy?.broad_match_penalty || 0); | ||
| const REVIEWED_DOMAIN_STYLE_BRANDS = new Set(['copy.ai']); | ||
| const AI_FILE_CONTEXT_TERMS = new Set([ | ||
| 'adobe', | ||
| 'document', | ||
| 'documents', | ||
| 'download', | ||
| 'downloads', | ||
| 'export', | ||
| 'exports', | ||
| 'file', | ||
| 'files', | ||
| 'illustrator', | ||
| 'import', | ||
| 'imports', | ||
| 'upload', | ||
| 'uploads', | ||
| ]); | ||
| const NEGATIVE_STATE_TOKENS = new Set([ | ||
@@ -205,2 +222,22 @@ 'blocked', | ||
| export function hasDomainStyleBrandIntent(query) { | ||
| const normalizedQuery = String(query || '').normalize('NFKC').toLowerCase(); | ||
| const domainMatches = [ | ||
| ...normalizedQuery.matchAll( | ||
| /(?:^|\s)([\p{L}\p{N}][\p{L}\p{N}._-]*\.ai)(?=$|[\s,.;:!?()[\]{}])/giu, | ||
| ), | ||
| ]; | ||
| if (domainMatches.length === 0) return false; | ||
| const queryTokens = new Set(tokenize(normalizedQuery)); | ||
| return domainMatches.some((match) => { | ||
| const domain = match[1].toLowerCase(); | ||
| if (REVIEWED_DOMAIN_STYLE_BRANDS.has(domain)) return true; | ||
| const stem = domain.slice(0, -3); | ||
| if (AI_FILE_CONTEXT_TERMS.has(stem)) return false; | ||
| return ![...AI_FILE_CONTEXT_TERMS].some((term) => queryTokens.has(term)); | ||
| }); | ||
| } | ||
| export function getBrandRankAdjustment(query, candidate = {}) { | ||
@@ -211,3 +248,3 @@ const iconRef = getCandidateIconRef(candidate); | ||
| const meaningfulQuery = getMeaningfulBrandQuery(query); | ||
| const explicitBrandIntent = hasBrandIntent(query); | ||
| const explicitBrandIntent = hasBrandIntent(query) || hasDomainStyleBrandIntent(query); | ||
| let ambiguousExactMatch = null; | ||
@@ -255,3 +292,5 @@ let substringMatchClass = null; | ||
| const identity = getCandidateBrandIdentity(candidate, iconRef); | ||
| if (identity && identity === meaningfulQuery) { | ||
| const compactIdentity = identity.replace(/\s+/g, ''); | ||
| const compactQuery = meaningfulQuery.replace(/\s+/g, ''); | ||
| if (identity && (identity === meaningfulQuery || compactIdentity === compactQuery)) { | ||
| return { boost: 160, penalty: 0, match_class: 'distinctive_exact' }; | ||
@@ -269,2 +308,6 @@ } | ||
| if (hasDomainStyleBrandIntent(query)) { | ||
| return { boost: 0, penalty: 1000, match_class: 'non_brand_for_brand_query' }; | ||
| } | ||
| return { boost: 0, penalty: 0, match_class: null }; | ||
@@ -271,0 +314,0 @@ } |
@@ -926,2 +926,7 @@ const BASE_FILTER_TAGS = Object.freeze(['agentic-ai-tools-pack', 'brand-logo']); | ||
| ), | ||
| 'si:train-front': taxonomy( | ||
| 'cars-vehicles', | ||
| ['everyday-objects'], | ||
| ['train', 'subway', 'metro', 'transit'] | ||
| ), | ||
| }); | ||
@@ -928,0 +933,0 @@ |
+97
-13
@@ -5,2 +5,7 @@ import { normalizeSearchQueryRequest } from './search-query-normalization.js'; | ||
| 'Use search_icons as the main tool when a user asks for icons.', | ||
| 'If the user did not name a library, search all libraries. Never infer si from the Supericons server or app name.', | ||
| 'If you choose a library yourself, use prefer mode so relevant alternatives can be found elsewhere.', | ||
| 'Use strict mode only when the user explicitly requires that library.', | ||
| 'The library key si means Supericons. The library key simpleicons means Simple Icons.', | ||
| 'Use recommend_icons first for two or more named UI slots.', | ||
| 'When search_icons returns markdown_image, include it in the final answer so the user can see the result set.', | ||
@@ -12,2 +17,4 @@ 'The suggested_response_markdown field is safe to use as a compact answer, or it can be rewritten without changing the icon names and refs.', | ||
| 'Use preview_icons only to refine a result set or preview known icon refs.', | ||
| 'Use get_icon only with an exact returned ref. Do not guess icon IDs.', | ||
| 'When visual comparison matters, include preview_url as a visible fallback even when an inline image was requested.', | ||
| ].join(' '); | ||
@@ -81,2 +88,13 @@ | ||
| function normalizeOptionalLibrary(value, warnings) { | ||
| const requested = String(value || '').trim(); | ||
| if (!requested) return undefined; | ||
| const normalized = requested.toLowerCase(); | ||
| if (normalized === 'all' || normalized === 'any') { | ||
| warnings.push(`Treated library "${requested}" as all libraries with no library restriction.`); | ||
| return undefined; | ||
| } | ||
| return normalized; | ||
| } | ||
| export function normalizeSupportedLocale(value, supportedLocales = []) { | ||
@@ -109,2 +127,3 @@ const requested = String(value || '').trim().replace(/_/g, '-'); | ||
| const warnings = []; | ||
| const library = normalizeOptionalLibrary(args.library, warnings); | ||
| const locale = normalizeSupportedLocale(args.locale, supportedLocales); | ||
@@ -122,2 +141,13 @@ if (args.locale && !locale) { | ||
| const queryNormalization = normalizeSearchQueryRequest(args.query, normalizedStyle); | ||
| let libraryMode = normalizeChoice( | ||
| args.library_mode, | ||
| SEARCH_LIBRARY_MODES, | ||
| library ? 'strict' : 'all', | ||
| 'library_mode', | ||
| warnings, | ||
| ); | ||
| if (!library && libraryMode !== 'all') { | ||
| warnings.push(`Changed library_mode "${libraryMode}" to "all" because prefer and strict require a named library.`); | ||
| libraryMode = 'all'; | ||
| } | ||
| return { | ||
@@ -127,9 +157,4 @@ ...args, | ||
| original_query: queryNormalization.original_query, | ||
| library_mode: normalizeChoice( | ||
| args.library_mode, | ||
| SEARCH_LIBRARY_MODES, | ||
| 'strict', | ||
| 'library_mode', | ||
| warnings, | ||
| ), | ||
| library, | ||
| library_mode: libraryMode, | ||
| style: queryNormalization.style, | ||
@@ -394,2 +419,14 @@ locale, | ||
| function getIconFitReason(icon = {}) { | ||
| const semantic = icon.semantic || {}; | ||
| const reason = icon.why_it_fits | ||
| || icon.reason | ||
| || semantic.purpose | ||
| || semantic.use_when | ||
| || semantic.depicts; | ||
| if (!reason) return null; | ||
| const compact = String(reason).replace(/\s+/g, ' ').trim(); | ||
| return compact.length > 120 ? `${compact.slice(0, 117)}...` : compact; | ||
| } | ||
| export function buildSearchMatchPresentation({ | ||
@@ -406,10 +443,17 @@ query, | ||
| const name = icon.name || icon.id || ref || 'Icon'; | ||
| const reason = getIconFitReason(icon); | ||
| const suffix = reason ? `: ${escapeMarkdownInline(reason)}` : ''; | ||
| return ref | ||
| ? `- ${escapeMarkdownInline(name)} (\`${escapeMarkdownInline(ref)}\`)` | ||
| : `- ${escapeMarkdownInline(name)}`; | ||
| ? `- ${escapeMarkdownInline(name)} (\`${escapeMarkdownInline(ref)}\`)${suffix}` | ||
| : `- ${escapeMarkdownInline(name)}${suffix}`; | ||
| }); | ||
| const resultInterpretation = results.length === 1 | ||
| ? 'A single verified result is not automatically weak. For a specific query, it can be the complete answer when its meaning fits.' | ||
| : `Supericons returned ${results.length} verified options. Compare meaning and style before choosing the final ref.`; | ||
| const responseLines = [ | ||
| `Found ${results.length} icon option${results.length === 1 ? '' : 's'} for "${escapeMarkdownInline(query)}":`, | ||
| `Found ${results.length} verified icon option${results.length === 1 ? '' : 's'} for "${escapeMarkdownInline(query)}":`, | ||
| '', | ||
| ...itemLines, | ||
| '', | ||
| resultInterpretation, | ||
| ]; | ||
@@ -423,2 +467,6 @@ if (markdownImage) { | ||
| return { | ||
| outcome_type: 'results', | ||
| result_count: results.length, | ||
| top_result_ref: getIconRef(results[0]) || null, | ||
| result_interpretation: resultInterpretation, | ||
| image_url: imageUrl, | ||
@@ -431,7 +479,40 @@ markdown_image: markdownImage, | ||
| export function buildSearchNoResultPresentation({ query, hint } = {}) { | ||
| const safeHint = String(hint || 'Try a broader term or remove optional filters.').trim(); | ||
| export function buildSearchNoResultPresentation({ | ||
| query, | ||
| hint, | ||
| requestedLibrary, | ||
| alternativeResults = [], | ||
| } = {}) { | ||
| const recoveryQueryTokens = String(query || '') | ||
| .normalize('NFKC') | ||
| .toLowerCase() | ||
| .split(/[^\p{L}\p{N}]+/u) | ||
| .filter((token) => token.length >= 3 && !['brand', 'icon', 'icons', 'logo'].includes(token)); | ||
| const alternateRefs = [...new Set( | ||
| alternativeResults | ||
| .map(getIconRef) | ||
| .filter((ref) => { | ||
| if (!ref || recoveryQueryTokens.length === 0) return false; | ||
| const normalizedRef = ref | ||
| .normalize('NFKC') | ||
| .toLowerCase() | ||
| .replace(/[^\p{L}\p{N}]+/gu, ' '); | ||
| return recoveryQueryTokens.some((token) => normalizedRef.includes(token)); | ||
| }), | ||
| )].slice(0, 3); | ||
| const strictLibrary = String(requestedLibrary || '').trim(); | ||
| const hasAlternatives = strictLibrary && alternateRefs.length > 0; | ||
| const safeHint = hasAlternatives | ||
| ? `The requested ${strictLibrary} library had no match. Relevant alternatives exist elsewhere: ${alternateRefs.join(', ')}.` | ||
| : String(hint || 'Try a broader term or remove optional filters.').trim(); | ||
| const nextStep = hasAlternatives | ||
| ? `Use get_icon with an exact alternate ref such as ${alternateRefs[0]}, or retry search_icons once without library and with library_mode "all".` | ||
| : safeHint; | ||
| return { | ||
| outcome_type: 'no_match', | ||
| result_count: 0, | ||
| result_interpretation: 'No verified icon matched this query and its current filters. This is a no-result, not a service error.', | ||
| hint: safeHint, | ||
| suggested_response_markdown: `No matching icons were found for "${escapeMarkdownInline(query)}". ${safeHint}`, | ||
| next_step: safeHint, | ||
| next_step: nextStep, | ||
| }; | ||
@@ -456,2 +537,5 @@ } | ||
| return { | ||
| outcome_type: 'tool_error', | ||
| result_count: 0, | ||
| result_interpretation: 'The search tool could not complete the request. This is not a no-result and should not be described as poor search relevance.', | ||
| error: typeof error?.message === 'string' ? error.message : fallbackMessage, | ||
@@ -458,0 +542,0 @@ ...(typeof error?.code === 'string' ? { code: error.code } : {}), |
+2
-2
@@ -10,3 +10,3 @@ { | ||
| }, | ||
| "version": "0.4.26", | ||
| "version": "0.4.27", | ||
| "remotes": [ | ||
@@ -22,3 +22,3 @@ { | ||
| "identifier": "@supericons/mcp", | ||
| "version": "0.4.26", | ||
| "version": "0.4.27", | ||
| "transport": { | ||
@@ -25,0 +25,0 @@ "type": "stdio" |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
25986693
0.12%121368
0.2%