@corva/ui 3.78.0-15 → 3.78.0-16
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/mcp-server/server.mjs +73 -39
- package/package.json +1 -1
package/mcp-server/server.mjs
CHANGED
|
@@ -246,9 +246,9 @@ const validateDsn = dsn => {
|
|
|
246
246
|
return url.href;
|
|
247
247
|
};
|
|
248
248
|
|
|
249
|
-
const MCP_SERVER_VERSION = '1.
|
|
249
|
+
const MCP_SERVER_VERSION = '1.8.0';
|
|
250
250
|
|
|
251
|
-
var version = "3.78.0-
|
|
251
|
+
var version = "3.78.0-16";
|
|
252
252
|
|
|
253
253
|
const CORVA_UI_VERSION = version;
|
|
254
254
|
|
|
@@ -1055,6 +1055,8 @@ const SEARCH_CONFIG = {
|
|
|
1055
1055
|
exactMatch: 100,
|
|
1056
1056
|
startsWith: 50,
|
|
1057
1057
|
contains: 30,
|
|
1058
|
+
// Above keywordExact: a query word that names the entry beats one its keywords merely carry.
|
|
1059
|
+
nameWord: 25,
|
|
1058
1060
|
keywordExact: 20,
|
|
1059
1061
|
keywordContains: 10,
|
|
1060
1062
|
description: 5,
|
|
@@ -1064,7 +1066,9 @@ const SEARCH_CONFIG = {
|
|
|
1064
1066
|
v2Component: 10
|
|
1065
1067
|
},
|
|
1066
1068
|
defaults: {
|
|
1067
|
-
maxResults: 10
|
|
1069
|
+
maxResults: 10,
|
|
1070
|
+
// Outside `limit`: the fallback answers "does the other version have it", not "show more".
|
|
1071
|
+
maxFallbackResults: 5
|
|
1068
1072
|
}
|
|
1069
1073
|
};
|
|
1070
1074
|
|
|
@@ -21548,6 +21552,35 @@ const withTimeout = async (promise, ms) => {
|
|
|
21548
21552
|
*/
|
|
21549
21553
|
const singleLine = text => text.replace(/\s+/g, ' ').trim();
|
|
21550
21554
|
|
|
21555
|
+
const parseQuery = queryLower => {
|
|
21556
|
+
const rawWords = queryLower.trim().split(/\s+/);
|
|
21557
|
+
const queryWords = rawWords.filter(w => w.length > 1);
|
|
21558
|
+
const isMultiWord = rawWords.length >= 2 && queryWords.length >= 2;
|
|
21559
|
+
// When the query has filler words around a single meaningful word (e.g. "a button"),
|
|
21560
|
+
// the single-word path should match against just that word instead of the whole query.
|
|
21561
|
+
const singleWordQuery = !isMultiWord && queryWords.length === 1 ? queryWords[0] : queryLower;
|
|
21562
|
+
return {
|
|
21563
|
+
queryWords,
|
|
21564
|
+
isMultiWord,
|
|
21565
|
+
singleWordQuery
|
|
21566
|
+
};
|
|
21567
|
+
};
|
|
21568
|
+
|
|
21569
|
+
/**
|
|
21570
|
+
* Whether the name equals the query or starts with it — the query read the way the name scorers
|
|
21571
|
+
* read it: filler words dropped, a multi-word query collapsed ("date picker" → "datepicker").
|
|
21572
|
+
* Only such a match proves the version holds what was asked for; a keyword or prose hit does not.
|
|
21573
|
+
*/
|
|
21574
|
+
const hasNameMatch = (name, query) => {
|
|
21575
|
+
const {
|
|
21576
|
+
queryWords,
|
|
21577
|
+
isMultiWord,
|
|
21578
|
+
singleWordQuery
|
|
21579
|
+
} = parseQuery(query.toLowerCase());
|
|
21580
|
+
const needle = (isMultiWord ? queryWords.join('') : singleWordQuery).trim();
|
|
21581
|
+
return needle.length > 0 && name.toLowerCase().startsWith(needle);
|
|
21582
|
+
};
|
|
21583
|
+
|
|
21551
21584
|
/**
|
|
21552
21585
|
* "icon" says which kind of result is wanted, not what it shows: against an icon it would match
|
|
21553
21586
|
* every name, so "plus add icon" ranked arrows above PlusIcon. Other kinds keep the word —
|
|
@@ -21568,6 +21601,13 @@ const withoutIconStopWords = queryLower => queryLower.trim().split(/\s+/).filter
|
|
|
21568
21601
|
*/
|
|
21569
21602
|
const isBuriedIcon = (entry, queryLower) => entry.type === 'icon' && entry.isDeprecated === true && entry.name.toLowerCase() !== withoutIconStopWords(queryLower);
|
|
21570
21603
|
|
|
21604
|
+
/**
|
|
21605
|
+
* Whole-word match for prose: a substring would let "table" hit "editable", and such weak matches
|
|
21606
|
+
* would keep the V1/V2 fallback from ever showing the real `Table`. `_` is a boundary because
|
|
21607
|
+
* keywords carry constant names like `asset_types`. Both arguments are lower case.
|
|
21608
|
+
*/
|
|
21609
|
+
const includesWord = (textLower, word) => new RegExp(`(?<![a-z0-9])${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![a-z0-9])`).test(textLower);
|
|
21610
|
+
|
|
21571
21611
|
/**
|
|
21572
21612
|
* The best a keyword list can do for ONE query word. The single-word branch of `handleSearch` used
|
|
21573
21613
|
* to SUM over every keyword, so the score measured how long a keyword list is — `corvaAPI`'s 369
|
|
@@ -21581,12 +21621,12 @@ const scoreKeywordMatch = (keywords, word, weights) => keywords.reduce((best, ke
|
|
|
21581
21621
|
}, 0);
|
|
21582
21622
|
|
|
21583
21623
|
/**
|
|
21584
|
-
*
|
|
21585
|
-
*
|
|
21624
|
+
* The lower-case words of an entry name, for matching query words against. Acronym- and
|
|
21625
|
+
* `_`-aware — `NPT` → `npt`, `ASSET_TYPES` → `asset`, `types` — where the extractors'
|
|
21626
|
+
* `splitCamelCase` cuts at every capital and would leave such names no word to match. One-letter
|
|
21627
|
+
* pieces are dropped: a query word is never that short.
|
|
21586
21628
|
*/
|
|
21587
|
-
const
|
|
21588
|
-
return name.replace(/([A-Z])/g, ' $1').toLowerCase().trim().split(/\s+/).filter(w => w.length > 0);
|
|
21589
|
-
};
|
|
21629
|
+
const nameWords = name => (name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|\d+/g) ?? []).map(word => word.toLowerCase()).filter(word => word.length > 1);
|
|
21590
21630
|
|
|
21591
21631
|
const scoreMultiWordQuery = (entry, queryWords, weights) => {
|
|
21592
21632
|
let score = 0;
|
|
@@ -21605,7 +21645,7 @@ const scoreMultiWordQuery = (entry, queryWords, weights) => {
|
|
|
21605
21645
|
|
|
21606
21646
|
// 2. All-words-in-name-parts matching
|
|
21607
21647
|
let namePartsScore = 0;
|
|
21608
|
-
const nameParts =
|
|
21648
|
+
const nameParts = nameWords(entry.name);
|
|
21609
21649
|
const allWordsInName = queryWords.every(word => nameParts.some(part => part === word));
|
|
21610
21650
|
if (allWordsInName) {
|
|
21611
21651
|
namePartsScore = weights.contains;
|
|
@@ -21614,19 +21654,19 @@ const scoreMultiWordQuery = (entry, queryWords, weights) => {
|
|
|
21614
21654
|
// Take max of collapsed and name-parts
|
|
21615
21655
|
score += Math.max(collapsedNameScore, namePartsScore);
|
|
21616
21656
|
|
|
21617
|
-
// 3. Per-word keyword
|
|
21657
|
+
// 3. Per-word scoring: the best keyword per word, never a sum — unless the word is a word of the
|
|
21658
|
+
// name, which outweighs any keyword. Otherwise "map layers toggle" scores LayersIcon like every
|
|
21659
|
+
// icon that only has "toggle" as a keyword, and file order decides.
|
|
21618
21660
|
const descriptionLower = entry.description.toLowerCase();
|
|
21619
|
-
|
|
21620
|
-
// Behaviour-identical to the inline loop this replaced: the best keyword per word, never a sum.
|
|
21621
|
-
score += queryWords.reduce((keywordScore, word) => keywordScore + scoreKeywordMatch(entry.keywords, word, weights), 0);
|
|
21661
|
+
score += queryWords.reduce((wordScore, word) => wordScore + Math.max(nameParts.includes(word) ? weights.nameWord : 0, scoreKeywordMatch(entry.keywords, word, weights)), 0);
|
|
21622
21662
|
|
|
21623
21663
|
// 4. All-words-in-description
|
|
21624
|
-
if (queryWords.every(word => descriptionLower
|
|
21664
|
+
if (queryWords.every(word => includesWord(descriptionLower, word))) {
|
|
21625
21665
|
score += weights.description;
|
|
21626
21666
|
}
|
|
21627
21667
|
|
|
21628
21668
|
// 5. All-words-in-searchText
|
|
21629
|
-
if (queryWords.every(word => entry.searchText
|
|
21669
|
+
if (queryWords.every(word => includesWord(entry.searchText, word))) {
|
|
21630
21670
|
score += weights.fullText;
|
|
21631
21671
|
}
|
|
21632
21672
|
return score;
|
|
@@ -21641,19 +21681,6 @@ const deprecationOrder = entry => entry.isDeprecated ? 1 : 0;
|
|
|
21641
21681
|
* fell to generation order before, an accident of extractor discovery.
|
|
21642
21682
|
*/
|
|
21643
21683
|
const compareScoredEntries = queryLower => (a, b) => Number(isBuriedIcon(a.entry, queryLower)) - Number(isBuriedIcon(b.entry, queryLower)) || b.score - a.score || deprecationOrder(a.entry) - deprecationOrder(b.entry);
|
|
21644
|
-
const parseQuery = queryLower => {
|
|
21645
|
-
const rawWords = queryLower.trim().split(/\s+/);
|
|
21646
|
-
const queryWords = rawWords.filter(w => w.length > 1);
|
|
21647
|
-
const isMultiWord = rawWords.length >= 2 && queryWords.length >= 2;
|
|
21648
|
-
// When the query has filler words around a single meaningful word (e.g. "a button"),
|
|
21649
|
-
// the single-word path should match against just that word instead of the whole query.
|
|
21650
|
-
const singleWordQuery = !isMultiWord && queryWords.length === 1 ? queryWords[0] : queryLower;
|
|
21651
|
-
return {
|
|
21652
|
-
queryWords,
|
|
21653
|
-
isMultiWord,
|
|
21654
|
-
singleWordQuery
|
|
21655
|
-
};
|
|
21656
|
-
};
|
|
21657
21684
|
const rankEntries = (entries, query) => {
|
|
21658
21685
|
const {
|
|
21659
21686
|
weights,
|
|
@@ -21690,10 +21717,10 @@ const rankEntries = (entries, query) => {
|
|
|
21690
21717
|
|
|
21691
21718
|
// One word, one keyword payment — the same rule the multi-word branch applies per word.
|
|
21692
21719
|
score += scoreKeywordMatch(entry.keywords, singleWordQuery, weights);
|
|
21693
|
-
if (entry.description.toLowerCase()
|
|
21720
|
+
if (includesWord(entry.description.toLowerCase(), singleWordQuery)) {
|
|
21694
21721
|
score += weights.description;
|
|
21695
21722
|
}
|
|
21696
|
-
if (entry.searchText
|
|
21723
|
+
if (includesWord(entry.searchText, singleWordQuery)) {
|
|
21697
21724
|
score += weights.fullText;
|
|
21698
21725
|
}
|
|
21699
21726
|
}
|
|
@@ -21748,12 +21775,12 @@ Examples:
|
|
|
21748
21775
|
- "date picker" - find date/time components
|
|
21749
21776
|
- "useSubscriptions" - find specific hook
|
|
21750
21777
|
|
|
21751
|
-
When category is 'v2' or 'v1' and the requested version
|
|
21778
|
+
When category is 'v2' or 'v1', type is 'all' or 'component', and no component of the requested version is named by the query (its name equals or starts with it), up to 5 matching components of the other version follow the results, labeled, so a single search still discovers a candidate. The agent should still prefer V2 when both versions match.`;
|
|
21752
21779
|
const searchToolSchema = {
|
|
21753
21780
|
query: z.string().describe('Search query - component name, use case, or description'),
|
|
21754
21781
|
type: z.enum(SEARCH_TYPES).optional().describe('Filter by type (default: all)'),
|
|
21755
21782
|
category: z.enum(['all', 'v2', 'v1']).optional().describe('For components: v2 (modern) or v1 (default: all)'),
|
|
21756
|
-
limit: z.number().optional().describe('Maximum results to return (default: 10)')
|
|
21783
|
+
limit: z.number().optional().describe('Maximum results to return (default: 10); a V1/V2 fallback adds up to 5 more')
|
|
21757
21784
|
};
|
|
21758
21785
|
const handleSearch = args => {
|
|
21759
21786
|
const {
|
|
@@ -21784,7 +21811,10 @@ const handleSearch = args => {
|
|
|
21784
21811
|
}
|
|
21785
21812
|
const matchesRequested = entry => requestedCategory === null || entry.type !== COMPONENT_TYPE || entry.category === requestedCategory;
|
|
21786
21813
|
const primary = ranked.filter(matchesRequested).slice(0, limit);
|
|
21787
|
-
|
|
21814
|
+
// Weak matches — a keyword, a word of prose, an icon — do not prove the requested version has the
|
|
21815
|
+
// component, so they must not hide the other version's. Read before `limit` cuts the list.
|
|
21816
|
+
const requestedHasName = ranked.some(entry => entry.type === COMPONENT_TYPE && entry.category === requestedCategory && hasNameMatch(entry.name, query));
|
|
21817
|
+
const fallback = requestedCategory && (type === 'all' || type === COMPONENT_TYPE) && !requestedHasName ? ranked.filter(entry => entry.type === COMPONENT_TYPE && entry.category !== requestedCategory).slice(0, SEARCH_CONFIG.defaults.maxFallbackResults) : [];
|
|
21788
21818
|
if (primary.length === 0 && fallback.length === 0) {
|
|
21789
21819
|
return {
|
|
21790
21820
|
response: createToolResponse(`No results found for ${echoedQuery}. Try:\n- A different search term\n- Using list_corva_ui to see available items`),
|
|
@@ -21801,20 +21831,24 @@ const handleSearch = args => {
|
|
|
21801
21831
|
const fields = [notice, singleLine(r.description) || NO_DESCRIPTION, ...importLines(r)];
|
|
21802
21832
|
return [`**${r.name}**${categoryLabel} - ${r.type}`, ...fields.filter(Boolean).map(field => `${INDENT}${field}`)].join('\n');
|
|
21803
21833
|
};
|
|
21804
|
-
|
|
21805
|
-
|
|
21834
|
+
const formatted = [...primary, ...fallback].map(formatEntry).join('\n\n');
|
|
21835
|
+
const resultCount = primary.length + fallback.length;
|
|
21836
|
+
if (fallback.length === 0) {
|
|
21806
21837
|
return {
|
|
21807
21838
|
response: createToolResponse(`Found ${primary.length} result(s) for ${echoedQuery}:\n\n${formatted}`),
|
|
21808
|
-
resultCount
|
|
21839
|
+
resultCount
|
|
21809
21840
|
};
|
|
21810
21841
|
}
|
|
21811
21842
|
const requestedVersion = requestedCategory.replace(COMPONENTS_CATEGORY_PREFIX, '').toUpperCase();
|
|
21812
21843
|
const fallbackVersion = requestedVersion === 'V2' ? 'V1' : 'V2';
|
|
21813
21844
|
const preferenceHint = requestedVersion === 'V2' ? ' V2 is preferred when available; consider these only if no V2 alternative exists.' : '';
|
|
21814
|
-
|
|
21845
|
+
// The label lives in the preamble, not in a block of its own: the blank line separates results
|
|
21846
|
+
// and nothing else, so a consumer can still count and split them. Each fallback block carries
|
|
21847
|
+
// its version in its heading.
|
|
21848
|
+
const preamble = primary.length === 0 ? `No ${requestedVersion} matches for ${echoedQuery}. ${fallbackVersion} fallback — ${fallbackVersion} components that match the query.` : `Found ${primary.length} result(s) for ${echoedQuery}, but no ${requestedVersion} component name equals or starts with it. The last ${fallback.length} result(s) are the ${fallbackVersion} fallback — ${fallbackVersion} components that match the query.`;
|
|
21815
21849
|
return {
|
|
21816
|
-
response: createToolResponse(
|
|
21817
|
-
resultCount
|
|
21850
|
+
response: createToolResponse(`${preamble}${preferenceHint}\n\n${formatted}`),
|
|
21851
|
+
resultCount
|
|
21818
21852
|
};
|
|
21819
21853
|
};
|
|
21820
21854
|
|