@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/CHANGELOG.md +78 -0
- package/auth.js +69 -0
- package/converter.js +8 -0
- package/generated/mcp-output-locales.json +12436 -0
- package/generated/motion-lab-baseline.json +886 -0
- package/hosted-search-client.js +198 -0
- package/index.js +1240 -0
- package/material-export.js +174 -0
- package/mcp-output-localization.js +132 -0
- package/motion-lab-client.js +347 -0
- package/motion-lab.js +21 -0
- package/package.json +63 -0
- package/public/cjk-search-terms.json +63474 -0
- package/public/multilingual-search-aliases.json +4307 -0
- package/public/product-facts.json +25 -0
- package/recommend-icons.js +707 -0
- package/remote-server.js +465 -0
- package/runtime/cjk-search-core.js +82 -0
- package/runtime/converter-workflow.js +593 -0
- package/runtime/generated-search-intent-rules.js +1190 -0
- package/runtime/icon-semantic-aliases.js +330 -0
- package/runtime/icon-taxonomy-seed.js +461 -0
- package/runtime/public-metadata-sanitizer.js +171 -0
- package/runtime/search-intent-core.js +130 -0
- package/search.js +375 -0
- package/semantic-registry.js +212 -0
- package/server.json +27 -0
- package/telemetry.js +85 -0
- package/variant-support.js +236 -0
- package/workflow-access.js +65 -0
package/remote-server.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Supericons hosted MCP server.
|
|
4
|
+
*
|
|
5
|
+
* This exposes a Streamable HTTP MCP endpoint for hosted directories and agents.
|
|
6
|
+
* The local stdio package in index.js remains the main IDE setup.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
13
|
+
import express from 'express';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { searchIconsHostedMcp } from './hosted-search-client.js';
|
|
16
|
+
import { searchIcons as searchLocalIcons } from './search.js';
|
|
17
|
+
import { recommendIconsForTask } from './recommend-icons.js';
|
|
18
|
+
import {
|
|
19
|
+
buildPublicSemanticPayload,
|
|
20
|
+
createSemanticRegistryMap,
|
|
21
|
+
getSemanticRecordForIcon,
|
|
22
|
+
loadSemanticRegistryRecords,
|
|
23
|
+
} from './semantic-registry.js';
|
|
24
|
+
|
|
25
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const dataDir = join(__dirname, 'public');
|
|
27
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
|
28
|
+
const productFacts = JSON.parse(readFileSync(join(dataDir, 'product-facts.json'), 'utf8'));
|
|
29
|
+
const registrySummary = JSON.parse(readFileSync(join(dataDir, 'registry-summary.json'), 'utf8'));
|
|
30
|
+
const iconIndexPath = join(dataDir, 'icon-index.json');
|
|
31
|
+
const solidIconIndexPath = join(dataDir, 'icon-index-solid.json');
|
|
32
|
+
const synonymsPath = join(dataDir, 'synonyms.json');
|
|
33
|
+
const iconIndex = existsSync(iconIndexPath) ? JSON.parse(readFileSync(iconIndexPath, 'utf8')) : { icons: [] };
|
|
34
|
+
const solidIconIndex = existsSync(solidIconIndexPath) ? JSON.parse(readFileSync(solidIconIndexPath, 'utf8')) : { icons: [] };
|
|
35
|
+
const synonyms = existsSync(synonymsPath) ? JSON.parse(readFileSync(synonymsPath, 'utf8')) : {};
|
|
36
|
+
const publicIcons = [
|
|
37
|
+
...(Array.isArray(iconIndex?.icons) ? iconIndex.icons : []),
|
|
38
|
+
...(Array.isArray(solidIconIndex?.icons) ? solidIconIndex.icons : []),
|
|
39
|
+
];
|
|
40
|
+
const semanticMap = createSemanticRegistryMap(loadSemanticRegistryRecords(dataDir));
|
|
41
|
+
|
|
42
|
+
const LIBRARIES = [
|
|
43
|
+
['bootstrap', 'Bootstrap', 'Official Bootstrap SVG icons'],
|
|
44
|
+
['heroicons', 'Heroicons', 'Interface icons by Tailwind Labs'],
|
|
45
|
+
['iconoir', 'Iconoir', 'Open-source outline and solid icons'],
|
|
46
|
+
['ionicons', 'Ionicons', 'Icons for app and interface design'],
|
|
47
|
+
['lucide', 'Lucide', 'Consistent open-source outline icons'],
|
|
48
|
+
['material', 'Material Symbols', 'Google Material Symbols'],
|
|
49
|
+
['mingcute', 'MingCute', 'Modern interface icons'],
|
|
50
|
+
['phosphor', 'Phosphor', 'Flexible icon family'],
|
|
51
|
+
['simpleicons', 'Simple Icons', 'Brand and product icons'],
|
|
52
|
+
['tabler', 'Tabler', 'Large open-source SVG icon library'],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const libraryKeysDescription =
|
|
56
|
+
'Supported values include lucide, tabler, phosphor, heroicons, bootstrap, iconoir, ionicons, material, simpleicons, and mingcute.';
|
|
57
|
+
const multilingualLocaleValues = ['zh-Hans', 'zh-Hant', 'ja', 'ko', 'es', 'de', 'pt', 'ar', 'hi', 'vi', 'th'];
|
|
58
|
+
const multilingualLocaleDescription =
|
|
59
|
+
'Optional locale for multilingual search terms. Supported values: zh-Hans, zh-Hant, ja, ko, es, de, pt, ar, hi, vi, th.';
|
|
60
|
+
|
|
61
|
+
const readOnlySearchAnnotations = {
|
|
62
|
+
readOnlyHint: true,
|
|
63
|
+
destructiveHint: false,
|
|
64
|
+
idempotentHint: true,
|
|
65
|
+
openWorldHint: false,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const publicIconResultSchema = z.object({
|
|
69
|
+
id: z.string().describe('Icon ID without the library prefix.'),
|
|
70
|
+
name: z.string().describe('Human-readable icon name.'),
|
|
71
|
+
library: z.string().describe('Source icon library key.'),
|
|
72
|
+
type: z.string().describe('Icon asset type, normally svg.'),
|
|
73
|
+
style: z.string().describe('Icon style such as outline or solid.'),
|
|
74
|
+
svg: z.string().describe('Inline SVG markup for the icon.'),
|
|
75
|
+
semantic: z.record(z.unknown()).nullable().optional().describe('Public semantic guidance for search and agent selection.'),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const libraryResultSchema = z.object({
|
|
79
|
+
id: z.string().describe('Library key used in tool calls.'),
|
|
80
|
+
name: z.string().describe('Human-readable library name.'),
|
|
81
|
+
description: z.string().describe('Brief public description of the icon library.'),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const searchIconsOutputSchema = {
|
|
85
|
+
results: z.array(publicIconResultSchema).describe('Matching icons with SVG code and semantic guidance.'),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const recommendIconsOutputSchema = {
|
|
89
|
+
task: z.string().describe('Original UI task.'),
|
|
90
|
+
library: z.string().optional().describe('Library filter used for recommendations, if provided.'),
|
|
91
|
+
style: z.string().optional().describe('Style preference used for recommendations.'),
|
|
92
|
+
slot_count: z.number().describe('Number of UI slots requested.'),
|
|
93
|
+
results: z.array(z.record(z.unknown())).describe('Recommended icon choices grouped by requested UI slot.'),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const getIconOutputSchema = {
|
|
97
|
+
icon: publicIconResultSchema.optional().describe('Exact matching icon when found.'),
|
|
98
|
+
error: z.string().optional().describe('Recoverable error message when no exact icon is found.'),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const listLibrariesOutputSchema = {
|
|
102
|
+
libraries: z.array(libraryResultSchema).describe('Free icon libraries available through this hosted MCP server.'),
|
|
103
|
+
publicRecordCount: z.number().describe('Number of public semantic icon records searchable through the hosted MCP server.'),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function asStructured(payload, { isError = false } = {}) {
|
|
107
|
+
return {
|
|
108
|
+
structuredContent: payload,
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: JSON.stringify(payload, null, 2),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
...(isError ? { isError: true } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeHostedIcon(row) {
|
|
120
|
+
if (!row?.icon_id) return null;
|
|
121
|
+
const [libraryFromId, ...idParts] = String(row.icon_id).split(':');
|
|
122
|
+
const library = row.library || row.source_library || libraryFromId;
|
|
123
|
+
const id = idParts.join(':') || row.id || row.name;
|
|
124
|
+
if (!library || !id || !row.svg) return null;
|
|
125
|
+
|
|
126
|
+
const icon = {
|
|
127
|
+
id,
|
|
128
|
+
name: row.name || id.replace(/[-_]/g, ' '),
|
|
129
|
+
library,
|
|
130
|
+
lib: library,
|
|
131
|
+
type: row.icon_type || 'svg',
|
|
132
|
+
style: row.style || 'outline',
|
|
133
|
+
svg: row.svg,
|
|
134
|
+
semantic: row.semantic || null,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const semanticRecord = getSemanticRecordForIcon(semanticMap, library, id);
|
|
138
|
+
return {
|
|
139
|
+
...icon,
|
|
140
|
+
semantic: buildPublicSemanticPayload(semanticRecord) || icon.semantic || null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeLocalIcon(icon) {
|
|
145
|
+
if (!icon?.id || !icon?.lib || !icon?.svg) return null;
|
|
146
|
+
|
|
147
|
+
const semanticRecord = getSemanticRecordForIcon(semanticMap, icon.lib, icon.id);
|
|
148
|
+
return {
|
|
149
|
+
id: icon.id,
|
|
150
|
+
name: icon.name || icon.id.replace(/[-_]/g, ' '),
|
|
151
|
+
library: icon.lib,
|
|
152
|
+
lib: icon.lib,
|
|
153
|
+
type: icon.type || 'svg',
|
|
154
|
+
style: icon.style || 'outline',
|
|
155
|
+
svg: icon.svg,
|
|
156
|
+
semantic: buildPublicSemanticPayload(semanticRecord) || null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function searchLocalFallbackIcons({ query, library, style = 'any', limit = 20, locale = null }) {
|
|
161
|
+
if (publicIcons.length === 0) return [];
|
|
162
|
+
|
|
163
|
+
return searchLocalIcons(query, publicIcons, synonyms, {
|
|
164
|
+
library: library || null,
|
|
165
|
+
style,
|
|
166
|
+
limit,
|
|
167
|
+
locale,
|
|
168
|
+
})
|
|
169
|
+
.map(normalizeLocalIcon)
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
.slice(0, Math.max(1, limit));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function searchHostedIcons({ query, library, style = 'any', limit = 20, locale = null }) {
|
|
175
|
+
let payload;
|
|
176
|
+
try {
|
|
177
|
+
payload = await searchIconsHostedMcp({
|
|
178
|
+
query,
|
|
179
|
+
library: library || null,
|
|
180
|
+
style,
|
|
181
|
+
limit,
|
|
182
|
+
locale,
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const fallbackResults = searchLocalFallbackIcons({
|
|
186
|
+
query,
|
|
187
|
+
library,
|
|
188
|
+
style,
|
|
189
|
+
limit,
|
|
190
|
+
locale,
|
|
191
|
+
});
|
|
192
|
+
if (fallbackResults.length > 0) return fallbackResults;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const hostedResults = (payload.results || [])
|
|
197
|
+
.map(normalizeHostedIcon)
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.slice(0, Math.max(1, limit));
|
|
200
|
+
|
|
201
|
+
if (hostedResults.length > 0) {
|
|
202
|
+
return hostedResults;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const fallbackResults = searchLocalFallbackIcons({
|
|
206
|
+
query,
|
|
207
|
+
library,
|
|
208
|
+
style,
|
|
209
|
+
limit,
|
|
210
|
+
locale,
|
|
211
|
+
});
|
|
212
|
+
if (fallbackResults.length > 0) return fallbackResults;
|
|
213
|
+
|
|
214
|
+
return hostedResults;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildPublicIconResult(icon) {
|
|
218
|
+
return {
|
|
219
|
+
id: icon.id,
|
|
220
|
+
name: icon.name,
|
|
221
|
+
library: icon.library,
|
|
222
|
+
type: icon.type,
|
|
223
|
+
style: icon.style,
|
|
224
|
+
svg: icon.svg,
|
|
225
|
+
semantic: icon.semantic,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function createServer() {
|
|
230
|
+
const freeIconCountLabel =
|
|
231
|
+
productFacts?.display?.freeIconsAcrossLibrariesFreeLabel ||
|
|
232
|
+
`${registrySummary.publicRecordCount.toLocaleString()} searchable free icon records`;
|
|
233
|
+
|
|
234
|
+
const server = new McpServer({
|
|
235
|
+
name: 'supericons',
|
|
236
|
+
version: packageJson.version,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
server.registerTool(
|
|
240
|
+
'search_icons',
|
|
241
|
+
{
|
|
242
|
+
title: 'Search Icons',
|
|
243
|
+
description: `Search ${freeIconCountLabel} by meaning, label, visual description, tags, and synonyms. Use this when the user describes an icon concept such as "database", "user profile", "chill", "security", or "AI model". Returns matching icons with SVG code and public semantic guidance.`,
|
|
244
|
+
inputSchema: {
|
|
245
|
+
query: z.string().describe('Icon concept or search phrase, for example "database", "user profile", "chill", "trash", "upload cloud", "AI model", or "beautiful".'),
|
|
246
|
+
library: z.string().optional().describe(`Optional library key. ${libraryKeysDescription}`),
|
|
247
|
+
style: z.enum(['any', 'outline', 'solid']).optional().default('any').describe('Optional style preference. Use "any" unless the user asks for outline or solid icons.'),
|
|
248
|
+
locale: z.enum(multilingualLocaleValues).optional().describe(multilingualLocaleDescription),
|
|
249
|
+
limit: z.number().min(1).max(50).optional().default(10).describe('Maximum number of icons to return. Use 5-10 for browsing and 1-3 for quick agent choices.'),
|
|
250
|
+
},
|
|
251
|
+
outputSchema: searchIconsOutputSchema,
|
|
252
|
+
annotations: readOnlySearchAnnotations,
|
|
253
|
+
},
|
|
254
|
+
async ({ query, library, style, locale, limit }) => {
|
|
255
|
+
const results = await searchHostedIcons({ query, library, style, locale, limit });
|
|
256
|
+
return asStructured({
|
|
257
|
+
results: results.map(buildPublicIconResult),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
server.registerTool(
|
|
263
|
+
'recommend_icons',
|
|
264
|
+
{
|
|
265
|
+
title: 'Recommend Icons',
|
|
266
|
+
description: 'Recommend a coherent icon set for named UI slots in a product, app, dashboard, or navigation flow. Use this when the user needs several icons that should work together. Returns one recommendation and optional alternatives for each slot.',
|
|
267
|
+
inputSchema: {
|
|
268
|
+
task: z.string().describe('Overall UI task, for example "choose icons for an AI dashboard sidebar" or "select bottom navigation icons for a finance app".'),
|
|
269
|
+
slots: z.array(z.string().min(1)).min(1).max(12).describe('List of UI slots to fill, for example ["model", "prompt", "dataset", "evaluation"].'),
|
|
270
|
+
library: z.string().optional().describe(`Optional library key when the user wants a consistent icon family. ${libraryKeysDescription}`),
|
|
271
|
+
style: z.enum(['any', 'outline', 'solid']).optional().default('any').describe('Optional style preference. Use "outline" for most sidebar and toolbar icon sets unless the user asks otherwise.'),
|
|
272
|
+
locale: z.enum(multilingualLocaleValues).optional().describe('Optional locale for multilingual slot labels. Supported values: zh-Hans, zh-Hant, ja, ko, es, de, pt, ar, hi, vi, th.'),
|
|
273
|
+
limit_per_slot: z.number().min(1).max(5).optional().default(3).describe('Number of choices to return for each slot. Use 1 for a final pick or 2-3 when the user wants alternatives.'),
|
|
274
|
+
},
|
|
275
|
+
outputSchema: recommendIconsOutputSchema,
|
|
276
|
+
annotations: readOnlySearchAnnotations,
|
|
277
|
+
},
|
|
278
|
+
async ({ task, slots, library, style, locale, limit_per_slot }) => {
|
|
279
|
+
const payload = await recommendIconsForTask({
|
|
280
|
+
task,
|
|
281
|
+
slots,
|
|
282
|
+
library,
|
|
283
|
+
style,
|
|
284
|
+
locale,
|
|
285
|
+
limitPerSlot: limit_per_slot,
|
|
286
|
+
semanticMap,
|
|
287
|
+
searchIconsForQuery: searchHostedIcons,
|
|
288
|
+
buildIconResult: async (icon) => buildPublicIconResult(icon),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return asStructured(payload);
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
server.registerTool(
|
|
296
|
+
'get_icon',
|
|
297
|
+
{
|
|
298
|
+
title: 'Get Icon',
|
|
299
|
+
description: 'Retrieve one exact SVG icon when the icon ID and library are already known. Use search_icons first if the user only described a concept. Returns SVG code and public semantic guidance for the exact icon.',
|
|
300
|
+
inputSchema: {
|
|
301
|
+
id: z.string().describe('Exact icon ID without the library prefix, for example "database", "user-circle", "brain-circuit", or "arrow-down".'),
|
|
302
|
+
library: z.string().describe(`Required library key for the exact icon. ${libraryKeysDescription}`),
|
|
303
|
+
style: z.enum(['any', 'outline', 'solid']).optional().default('any').describe('Optional style preference. Use "any" unless the caller needs a specific variant.'),
|
|
304
|
+
},
|
|
305
|
+
outputSchema: getIconOutputSchema,
|
|
306
|
+
annotations: readOnlySearchAnnotations,
|
|
307
|
+
},
|
|
308
|
+
async ({ id, library, style }) => {
|
|
309
|
+
const candidates = await searchHostedIcons({
|
|
310
|
+
query: id.replace(/[-_]+/g, ' '),
|
|
311
|
+
library,
|
|
312
|
+
style,
|
|
313
|
+
limit: 50,
|
|
314
|
+
});
|
|
315
|
+
const normalizedId = id.toLowerCase();
|
|
316
|
+
const match = candidates.find((icon) => icon.id.toLowerCase() === normalizedId);
|
|
317
|
+
|
|
318
|
+
if (!match) {
|
|
319
|
+
return asStructured({
|
|
320
|
+
error: `No matching icon found for ${library}:${id}.`,
|
|
321
|
+
}, { isError: true });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return asStructured({
|
|
325
|
+
icon: buildPublicIconResult(match),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
server.registerTool(
|
|
331
|
+
'list_libraries',
|
|
332
|
+
{
|
|
333
|
+
title: 'List Libraries',
|
|
334
|
+
description: 'List the free icon libraries available through the hosted Supericons MCP server. Use this before filtering by library or when a user asks which icon libraries are supported.',
|
|
335
|
+
outputSchema: listLibrariesOutputSchema,
|
|
336
|
+
annotations: readOnlySearchAnnotations,
|
|
337
|
+
},
|
|
338
|
+
async () => asStructured({
|
|
339
|
+
libraries: LIBRARIES.map(([id, name, description]) => ({
|
|
340
|
+
id,
|
|
341
|
+
name,
|
|
342
|
+
description,
|
|
343
|
+
})),
|
|
344
|
+
publicRecordCount: registrySummary.publicRecordCount,
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
return server;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function sendJson(res, status, payload) {
|
|
352
|
+
res.status(status).json(payload);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function buildServerCard(req) {
|
|
356
|
+
const host = req.get('host');
|
|
357
|
+
const protocol = req.get('x-forwarded-proto') || req.protocol || 'https';
|
|
358
|
+
const baseUrl = process.env.SUPERICONS_REMOTE_MCP_BASE_URL || `${protocol}://${host}`;
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
name: 'supericons',
|
|
362
|
+
displayName: 'Supericons',
|
|
363
|
+
description: 'Search and recommend Supericons SVG icons by meaning through MCP, including supported multilingual search terms.',
|
|
364
|
+
version: packageJson.version,
|
|
365
|
+
websiteUrl: 'https://supericons.dev',
|
|
366
|
+
transport: {
|
|
367
|
+
type: 'streamable-http',
|
|
368
|
+
url: `${baseUrl.replace(/\/+$/, '')}/mcp`,
|
|
369
|
+
},
|
|
370
|
+
configSchema: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {},
|
|
373
|
+
additionalProperties: false,
|
|
374
|
+
},
|
|
375
|
+
tools: [
|
|
376
|
+
'search_icons',
|
|
377
|
+
'recommend_icons',
|
|
378
|
+
'get_icon',
|
|
379
|
+
'list_libraries',
|
|
380
|
+
],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const app = express();
|
|
385
|
+
|
|
386
|
+
app.use(express.json({ limit: '1mb' }));
|
|
387
|
+
|
|
388
|
+
app.use((req, res, next) => {
|
|
389
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
390
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
|
391
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, MCP-Protocol-Version, Mcp-Session-Id');
|
|
392
|
+
if (req.method === 'OPTIONS') {
|
|
393
|
+
res.status(204).end();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
next();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
app.get('/health', (_req, res) => {
|
|
400
|
+
sendJson(res, 200, {
|
|
401
|
+
ok: true,
|
|
402
|
+
service: 'supericons-remote-mcp',
|
|
403
|
+
version: packageJson.version,
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
app.get('/.well-known/mcp/server-card.json', (req, res) => {
|
|
408
|
+
sendJson(res, 200, buildServerCard(req));
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
app.get('/.well-known/openai-apps-challenge', (_req, res) => {
|
|
412
|
+
const token = String(process.env.OPENAI_APPS_CHALLENGE_TOKEN || '').trim();
|
|
413
|
+
if (!token) {
|
|
414
|
+
res.status(404).type('text/plain').send('Not Found');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
res.status(200).type('text/plain').send(token);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
app.post('/mcp', async (req, res) => {
|
|
421
|
+
const server = createServer();
|
|
422
|
+
const transport = new StreamableHTTPServerTransport({
|
|
423
|
+
sessionIdGenerator: undefined,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
await server.connect(transport);
|
|
428
|
+
await transport.handleRequest(req, res, req.body);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error('Error handling MCP request:', error);
|
|
431
|
+
if (!res.headersSent) {
|
|
432
|
+
res.status(500).json({
|
|
433
|
+
jsonrpc: '2.0',
|
|
434
|
+
error: {
|
|
435
|
+
code: -32603,
|
|
436
|
+
message: 'Internal server error',
|
|
437
|
+
},
|
|
438
|
+
id: null,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
} finally {
|
|
442
|
+
res.on('close', () => {
|
|
443
|
+
void transport.close();
|
|
444
|
+
void server.close();
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
app.get('/mcp', (_req, res) => {
|
|
450
|
+
res.status(405).set('Allow', 'POST').send('Method Not Allowed');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
app.delete('/mcp', (_req, res) => {
|
|
454
|
+
res.status(405).set('Allow', 'POST').send('Method Not Allowed');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const port = Number(process.env.PORT || 3333);
|
|
458
|
+
|
|
459
|
+
app.listen(port, (error) => {
|
|
460
|
+
if (error) {
|
|
461
|
+
console.error('Failed to start hosted Supericons MCP server:', error);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
console.log(`Hosted Supericons MCP server listening on port ${port}`);
|
|
465
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const CJK_SEARCH_LOCALES = Object.freeze(['zh-Hans', 'zh-Hant', 'ja', 'ko']);
|
|
2
|
+
export const MULTILINGUAL_SEARCH_LOCALES = Object.freeze([...CJK_SEARCH_LOCALES, 'es', 'de', 'pt', 'ar', 'hi', 'vi', 'th']);
|
|
3
|
+
|
|
4
|
+
const SEPARATORS = /[_:\-]+/g;
|
|
5
|
+
const NON_SEARCH_CHARS = /[^\p{L}\p{M}\p{N}\s]/gu;
|
|
6
|
+
const CJK_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
7
|
+
const EXPLICIT_MULTILINGUAL_LOCALES = new Set(['es', 'de', 'pt', 'ar', 'hi', 'vi', 'th']);
|
|
8
|
+
const HANGUL_SPACING = /\s+/g;
|
|
9
|
+
|
|
10
|
+
export function normalizeCjkSearchText(value) {
|
|
11
|
+
return String(value || '')
|
|
12
|
+
.normalize('NFKC')
|
|
13
|
+
.toLocaleLowerCase()
|
|
14
|
+
.replace(SEPARATORS, ' ')
|
|
15
|
+
.replace(NON_SEARCH_CHARS, ' ')
|
|
16
|
+
.replace(/\s+/g, ' ')
|
|
17
|
+
.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compactKoreanSpacing(value) {
|
|
21
|
+
return normalizeCjkSearchText(value).replace(HANGUL_SPACING, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isLikelyCjkQuery(value) {
|
|
25
|
+
return CJK_SCRIPT.test(String(value || ''));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isSupportedMultilingualLocale(value) {
|
|
29
|
+
return MULTILINGUAL_SEARCH_LOCALES.includes(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function recordValues(record) {
|
|
33
|
+
return [
|
|
34
|
+
record.term,
|
|
35
|
+
...(record.variants || []),
|
|
36
|
+
].filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function recordMatchesQuery(record, normalizedQuery) {
|
|
40
|
+
if (!normalizedQuery) return false;
|
|
41
|
+
if (recordValues(record).some((value) => normalizeCjkSearchText(value) === normalizedQuery)) return true;
|
|
42
|
+
|
|
43
|
+
if (record.locale === 'ko') {
|
|
44
|
+
const compactQuery = compactKoreanSpacing(normalizedQuery);
|
|
45
|
+
return recordValues(record).some((value) => compactKoreanSpacing(value) === compactQuery);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function expandCjkQuery(query, options = {}) {
|
|
52
|
+
const normalizedQuery = normalizeCjkSearchText(query);
|
|
53
|
+
const locale = options.locale || null;
|
|
54
|
+
const canUseExplicitLocaleTerms = EXPLICIT_MULTILINGUAL_LOCALES.has(locale);
|
|
55
|
+
const canUseMultilingualTerms = isLikelyCjkQuery(query) || canUseExplicitLocaleTerms;
|
|
56
|
+
if (!normalizedQuery || !canUseMultilingualTerms) {
|
|
57
|
+
return { query: normalizedQuery, variants: [String(query || '').trim()].filter(Boolean), matched: [] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const terms = Array.isArray(options.terms) ? options.terms : [];
|
|
61
|
+
const eligibleTerms = locale
|
|
62
|
+
? terms.filter((record) => record.locale === locale)
|
|
63
|
+
: terms;
|
|
64
|
+
const matched = eligibleTerms.filter((record) => (
|
|
65
|
+
record.gate === 'auto_accept'
|
|
66
|
+
&& recordMatchesQuery(record, normalizedQuery)
|
|
67
|
+
));
|
|
68
|
+
const variants = [];
|
|
69
|
+
|
|
70
|
+
for (const value of [String(query || '').trim(), normalizedQuery]) {
|
|
71
|
+
if (value && !variants.includes(value)) variants.push(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const record of matched) {
|
|
75
|
+
for (const value of record.maps_to || []) {
|
|
76
|
+
const normalized = normalizeCjkSearchText(value);
|
|
77
|
+
if (normalized && !variants.includes(normalized)) variants.push(normalized);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { query: normalizedQuery, variants, matched };
|
|
82
|
+
}
|