varmory 1.0.8 → 1.0.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/README.md +17 -0
- package/dist/index.css +2 -2
- package/dist/varmory.mjs +4016 -13133
- package/dist/varmory.umd.js +257 -107
- package/mcp/showcaseMcp.js +434 -132
- package/package.json +11 -2
- package/scripts/buildSearchIndex.js +89 -0
package/mcp/showcaseMcp.js
CHANGED
|
@@ -399,6 +399,29 @@ function loadQuasarNormalizerData(rootDir) {
|
|
|
399
399
|
return { apiExtends, mixinLookup };
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
// ── Unified load pipeline ──────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Assemble and normalize the three in-memory caches from a varmory root dir
|
|
406
|
+
* and/or an explicit file list. Shared by `attachShowcase` (long-running MCP
|
|
407
|
+
* server) and `exportShowcaseDocs` (one-shot static export) so both see the
|
|
408
|
+
* exact same view of the project.
|
|
409
|
+
*/
|
|
410
|
+
function loadAll({ rootDir = null, files = [], quasar = true, maxDepth = 5 } = {}) {
|
|
411
|
+
const allFiles = [
|
|
412
|
+
...(rootDir ? collectFilesFromRoot(rootDir, { quasar, maxDepth }) : []),
|
|
413
|
+
...files,
|
|
414
|
+
];
|
|
415
|
+
const { categories, docs, definitions } = loadFromFiles(allFiles);
|
|
416
|
+
const normalizerData = quasar
|
|
417
|
+
? loadQuasarNormalizerData(rootDir)
|
|
418
|
+
: { apiExtends: {}, mixinLookup: {} };
|
|
419
|
+
for (const key of Object.keys(definitions)) {
|
|
420
|
+
definitions[key] = normalizeQuasarApi(definitions[key], normalizerData);
|
|
421
|
+
}
|
|
422
|
+
return { categories, docs, definitions };
|
|
423
|
+
}
|
|
424
|
+
|
|
402
425
|
// ── Markdown formatters (used by the get_api tool) ─────────────────────────
|
|
403
426
|
|
|
404
427
|
function formatType(type) {
|
|
@@ -426,6 +449,156 @@ function formatProps(apiDef) {
|
|
|
426
449
|
return lines.join('\n');
|
|
427
450
|
}
|
|
428
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Render a single showcase component as markdown: heading, category, imports,
|
|
454
|
+
* and the `<template>` snippet in a fenced html block. Used by both the
|
|
455
|
+
* `get_component` MCP tool and the static exporter.
|
|
456
|
+
*/
|
|
457
|
+
function renderComponentMd(item, cat) {
|
|
458
|
+
const lines = [`# ${item.label}`, `Category: ${cat}`];
|
|
459
|
+
const impNames = Array.isArray(item.importName)
|
|
460
|
+
? item.importName
|
|
461
|
+
: (item.importName ? [item.importName] : []);
|
|
462
|
+
for (const n of impNames) lines.push(`Import: ${n} from '${item.importFrom || 'varmory'}'`);
|
|
463
|
+
if (item.template) lines.push('', '## Template', '```html', item.template, '```');
|
|
464
|
+
return lines.join('\n');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Render a component's full API (props, slots, events, methods) as markdown.
|
|
469
|
+
* Used by both the `get_api` MCP tool and the static exporter.
|
|
470
|
+
*/
|
|
471
|
+
function renderApiMd(name, def) {
|
|
472
|
+
const lines = [`# ${name} API`, '', '## Props', formatProps(def)];
|
|
473
|
+
|
|
474
|
+
if (def.slots && Object.keys(def.slots).length) {
|
|
475
|
+
lines.push('', '## Slots');
|
|
476
|
+
for (const [slot, info] of Object.entries(def.slots)) {
|
|
477
|
+
lines.push(`- **${slot}** — ${info.desc || ''}${formatMixinTag(info)}`);
|
|
478
|
+
if (info.scope && Object.keys(info.scope).length) {
|
|
479
|
+
for (const [k, v] of Object.entries(info.scope)) {
|
|
480
|
+
lines.push(` - \`props.${k}\`: ${formatType(v?.type)} — ${v?.desc || ''}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (def.events && Object.keys(def.events).length) {
|
|
487
|
+
lines.push('', '## Events');
|
|
488
|
+
for (const [event, info] of Object.entries(def.events)) {
|
|
489
|
+
lines.push(`- **${event}** — ${info.desc || ''}${formatMixinTag(info)}`);
|
|
490
|
+
if (info.params && Object.keys(info.params).length) {
|
|
491
|
+
for (const [k, v] of Object.entries(info.params)) {
|
|
492
|
+
lines.push(` - \`${k}\`: ${formatType(v?.type)} — ${v?.desc || ''}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (def.methods && Object.keys(def.methods).length) {
|
|
499
|
+
lines.push('', '## Methods');
|
|
500
|
+
for (const [method, info] of Object.entries(def.methods)) {
|
|
501
|
+
lines.push(`- **${method}()** — ${info.desc || ''}${formatMixinTag(info)}`);
|
|
502
|
+
if (info.params && Object.keys(info.params).length) {
|
|
503
|
+
for (const [k, v] of Object.entries(info.params)) {
|
|
504
|
+
const req = v?.required ? ' *(required)*' : '';
|
|
505
|
+
lines.push(` - param \`${k}\`${req}: ${formatType(v?.type)} — ${v?.desc || ''}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (info.returns && typeof info.returns === 'object') {
|
|
509
|
+
lines.push(` - returns: ${formatType(info.returns.type)} — ${info.returns.desc || ''}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return lines.join('\n');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Doc enrichment ─────────────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Scan markdown for showcase hash links (`[label](#Cat/Name)` or
|
|
520
|
+
* `<a href="#Cat/Name">`) and inline each referenced item's `<template>` as
|
|
521
|
+
* a fenced html block right after the containing block (paragraph, heading,
|
|
522
|
+
* list, etc.) — never mid-line.
|
|
523
|
+
*
|
|
524
|
+
* Each `Cat/Name` is injected only on its first occurrence; later references
|
|
525
|
+
* stay as bare links. Links inside fenced code blocks are ignored, and
|
|
526
|
+
* unresolved refs are silently dropped. Returns the original content
|
|
527
|
+
* unchanged when no refs resolve.
|
|
528
|
+
*/
|
|
529
|
+
function enrichDocWithShowcaseLinks(content, categories) {
|
|
530
|
+
if (!content || !categories) return content;
|
|
531
|
+
|
|
532
|
+
const catKey = (cat) => {
|
|
533
|
+
if (categories[cat]) return cat;
|
|
534
|
+
const lc = cat.toLowerCase();
|
|
535
|
+
return Object.keys(categories).find(k => k.toLowerCase() === lc);
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const resolve = (cat, name) => {
|
|
539
|
+
const key = catKey(cat);
|
|
540
|
+
if (!key) return null;
|
|
541
|
+
const lc = name.toLowerCase();
|
|
542
|
+
const item = categories[key].find(i =>
|
|
543
|
+
i.name.toLowerCase() === lc || i.label.toLowerCase() === lc,
|
|
544
|
+
);
|
|
545
|
+
if (!item || !item.template) return null;
|
|
546
|
+
return { cat: key, item };
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const mdRe = /\[[^\]]+\]\(#([A-Za-z][A-Za-z0-9_-]*)\/([A-Za-z0-9_-]+)\)/g;
|
|
550
|
+
const htmlRe = /href\s*=\s*["']#([A-Za-z][A-Za-z0-9_-]*)\/([A-Za-z0-9_-]+)["']/g;
|
|
551
|
+
|
|
552
|
+
const out = [];
|
|
553
|
+
const injected = new Set();
|
|
554
|
+
let pending = [];
|
|
555
|
+
let inFence = false;
|
|
556
|
+
|
|
557
|
+
const flushPending = () => {
|
|
558
|
+
if (!pending.length) return;
|
|
559
|
+
// Ensure exactly one blank line sits between prior content and the
|
|
560
|
+
// first injection, regardless of whether we just pushed a blank.
|
|
561
|
+
if (out.length && out[out.length - 1].trim() !== '') out.push('');
|
|
562
|
+
for (const { cat, item } of pending) {
|
|
563
|
+
out.push(`<!-- showcase: ${cat}/${item.name} -->`);
|
|
564
|
+
out.push('```html');
|
|
565
|
+
out.push(item.template);
|
|
566
|
+
out.push('```');
|
|
567
|
+
out.push('');
|
|
568
|
+
}
|
|
569
|
+
pending = [];
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
for (const line of content.split('\n')) {
|
|
573
|
+
const isFenceMarker = /^\s*```/.test(line);
|
|
574
|
+
if (isFenceMarker) inFence = !inFence;
|
|
575
|
+
|
|
576
|
+
out.push(line);
|
|
577
|
+
|
|
578
|
+
if (!inFence && !isFenceMarker) {
|
|
579
|
+
for (const re of [mdRe, htmlRe]) {
|
|
580
|
+
re.lastIndex = 0;
|
|
581
|
+
let m;
|
|
582
|
+
while ((m = re.exec(line)) !== null) {
|
|
583
|
+
const key = `${m[1]}/${m[2]}`;
|
|
584
|
+
if (injected.has(key)) continue;
|
|
585
|
+
const res = resolve(m[1], m[2]);
|
|
586
|
+
if (!res) continue;
|
|
587
|
+
injected.add(key);
|
|
588
|
+
pending.push(res);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (!inFence && line.trim() === '' && pending.length) flushPending();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// EOF flush — only kicks in when the doc has no trailing blank line.
|
|
597
|
+
if (pending.length) flushPending();
|
|
598
|
+
|
|
599
|
+
return out.join('\n');
|
|
600
|
+
}
|
|
601
|
+
|
|
429
602
|
// ── Public entry point ─────────────────────────────────────────────────────
|
|
430
603
|
|
|
431
604
|
/**
|
|
@@ -447,80 +620,115 @@ function formatProps(apiDef) {
|
|
|
447
620
|
* but won't have access to Quasar's shared `extends` / mixin pool.
|
|
448
621
|
* @param {number} [options.maxDepth=5] - How deep the `categories/` /
|
|
449
622
|
* `definitions/` folder search goes under `rootDir`.
|
|
623
|
+
* @param {boolean} [options.inlineShowcaseExamples=true] - When true, docs
|
|
624
|
+
* served via `get_doc` / the `doc` resource are post-processed to
|
|
625
|
+
* append a `## Referenced showcase components` section with the
|
|
626
|
+
* `<template>` of every `#Category/Name` link they contain. Set false
|
|
627
|
+
* to serve raw markdown.
|
|
628
|
+
* @param {string|null} [options.searchIndex] - Absolute path to a pre-built
|
|
629
|
+
* `.vecito` search index. When provided, `search_components` and
|
|
630
|
+
* `search_docs` use semantic search. When omitted, defaults to
|
|
631
|
+
* `<rootDir>/src/public/search.vecito` if it exists, otherwise
|
|
632
|
+
* falls back to substring matching.
|
|
450
633
|
* @returns {McpServer} The same server instance, now with resources and tools
|
|
451
634
|
* attached.
|
|
452
635
|
*/
|
|
636
|
+
export { loadAll };
|
|
637
|
+
|
|
453
638
|
export default function attachShowcase(server, options = {}) {
|
|
454
639
|
const rootDir = options.rootDir || null;
|
|
455
640
|
const quasar = options.quasar !== false; // default true
|
|
456
641
|
const maxDepth = options.maxDepth ?? 5;
|
|
642
|
+
const inlineShowcaseExamples = options.inlineShowcaseExamples !== false;
|
|
457
643
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
];
|
|
465
|
-
const { categories, docs, definitions } = loadFromFiles(allFiles);
|
|
644
|
+
const { categories, docs, definitions } = loadAll({
|
|
645
|
+
rootDir,
|
|
646
|
+
files: options.files || [],
|
|
647
|
+
quasar,
|
|
648
|
+
maxDepth,
|
|
649
|
+
});
|
|
466
650
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
651
|
+
const serveDoc = (content) =>
|
|
652
|
+
inlineShowcaseExamples ? enrichDocWithShowcaseLinks(content, categories) : content;
|
|
653
|
+
|
|
654
|
+
// ── Vecito semantic search (loaded lazily from pre-built snapshot) ──
|
|
655
|
+
const vecitoReady = (() => {
|
|
656
|
+
if ('searchIndex' in options && !options.searchIndex) return null;
|
|
657
|
+
const snapshotPath = options.searchIndex
|
|
658
|
+
|| (rootDir ? path.join(rootDir, 'src/public/search.vecito') : null);
|
|
659
|
+
if (!snapshotPath || !fs.existsSync(snapshotPath)) return null;
|
|
660
|
+
return import('vecito').then(({ Vecito }) => Vecito.load(snapshotPath)).catch((err) => {
|
|
661
|
+
console.error('[varmory] Failed to load vecito search index:', err.message);
|
|
662
|
+
return null;
|
|
663
|
+
});
|
|
664
|
+
})();
|
|
665
|
+
|
|
666
|
+
/** Wrap a tool handler with try/catch so failures return isError instead of crashing. */
|
|
667
|
+
const safeTool = (name, fn) => async (args) => {
|
|
668
|
+
try { return await fn(args); }
|
|
669
|
+
catch (err) {
|
|
670
|
+
console.error(`[varmory] ${name}:`, err);
|
|
671
|
+
return { isError: true, content: [{ type: 'text', text: `Error in ${name}: ${err.message}` }] };
|
|
672
|
+
}
|
|
673
|
+
};
|
|
476
674
|
|
|
477
675
|
// ── Resources ──────────────────────────────────────────────────────
|
|
478
676
|
// Resources are URI-addressable listings; MCP clients typically enumerate
|
|
479
677
|
// them to discover what's available.
|
|
480
678
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
|
|
679
|
+
const docNames = Object.keys(docs);
|
|
680
|
+
const defNames = Object.keys(definitions);
|
|
681
|
+
|
|
682
|
+
server.resource(
|
|
683
|
+
'docs-list',
|
|
684
|
+
{ uri: 'showcase://docs', name: 'Documentation index', description: 'Lists all available documentation pages', mimeType: 'text/plain' },
|
|
685
|
+
async () => ({
|
|
686
|
+
contents: [{ uri: 'showcase://docs', text: `Available docs:\n${docNames.map(n => `- ${n}`).join('\n')}`, mimeType: 'text/plain' }],
|
|
687
|
+
}),
|
|
688
|
+
);
|
|
488
689
|
|
|
489
690
|
server.resource(
|
|
490
691
|
'doc',
|
|
491
|
-
new ResourceTemplate('showcase://docs/{name}', {
|
|
692
|
+
new ResourceTemplate('showcase://docs/{name}', {
|
|
693
|
+
list: async () => ({
|
|
694
|
+
resources: docNames.map(n => ({ uri: `showcase://docs/${n}`, name: n })),
|
|
695
|
+
}),
|
|
696
|
+
description: 'Read a specific documentation page by name',
|
|
697
|
+
}),
|
|
492
698
|
async (uri, { name }) => {
|
|
493
699
|
const content = docs[name];
|
|
494
|
-
if (!content)
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
};
|
|
700
|
+
if (!content) {
|
|
701
|
+
return { contents: [{ uri: uri.href, text: `Doc "${name}" not found. Available: ${docNames.join(', ')}`, mimeType: 'text/plain' }] };
|
|
702
|
+
}
|
|
703
|
+
return { contents: [{ uri: uri.href, text: serveDoc(content), mimeType: 'text/markdown' }] };
|
|
498
704
|
},
|
|
499
705
|
);
|
|
500
706
|
|
|
501
|
-
server.resource(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
707
|
+
server.resource(
|
|
708
|
+
'components-list',
|
|
709
|
+
{ uri: 'showcase://components', name: 'Components index', description: 'Lists all showcase components grouped by category', mimeType: 'text/markdown' },
|
|
710
|
+
async () => {
|
|
711
|
+
const lines = [];
|
|
712
|
+
for (const [cat, items] of Object.entries(categories)) {
|
|
713
|
+
lines.push(`## ${cat}`);
|
|
714
|
+
for (const item of items) {
|
|
715
|
+
const impNames = Array.isArray(item.importName) ? item.importName.join(', ') : item.importName;
|
|
716
|
+
const imp = impNames ? ` (${impNames})` : '';
|
|
717
|
+
lines.push(`- ${item.label}${imp}`);
|
|
718
|
+
}
|
|
719
|
+
lines.push('');
|
|
509
720
|
}
|
|
510
|
-
lines.
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
contents: [{ uri: 'showcase://components', text: lines.join('\n'), mimeType: 'text/markdown' }],
|
|
514
|
-
};
|
|
515
|
-
});
|
|
721
|
+
return { contents: [{ uri: 'showcase://components', text: lines.join('\n'), mimeType: 'text/markdown' }] };
|
|
722
|
+
},
|
|
723
|
+
);
|
|
516
724
|
|
|
517
|
-
server.resource(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
contents: [{ uri: 'showcase://definitions', text: `Available API definitions:\n${
|
|
522
|
-
}
|
|
523
|
-
|
|
725
|
+
server.resource(
|
|
726
|
+
'definitions-list',
|
|
727
|
+
{ uri: 'showcase://definitions', name: 'API definitions index', description: 'Lists all available component API definitions', mimeType: 'text/plain' },
|
|
728
|
+
async () => ({
|
|
729
|
+
contents: [{ uri: 'showcase://definitions', text: `Available API definitions:\n${defNames.map(n => `- ${n}`).join('\n')}`, mimeType: 'text/plain' }],
|
|
730
|
+
}),
|
|
731
|
+
);
|
|
524
732
|
|
|
525
733
|
// ── Tools ──────────────────────────────────────────────────────────
|
|
526
734
|
// Tools are functions the model can call. All of ours are read-only —
|
|
@@ -533,16 +741,23 @@ export default function attachShowcase(server, options = {}) {
|
|
|
533
741
|
server.tool(
|
|
534
742
|
'search_components',
|
|
535
743
|
'Search showcase components by name or label',
|
|
536
|
-
{ query: z.string().describe('Search term to match against component names and labels') },
|
|
744
|
+
{ query: z.string().min(1).describe('Search term to match against component names and labels') },
|
|
537
745
|
readOnly,
|
|
538
|
-
async ({ query }) => {
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
746
|
+
safeTool('search_components', async ({ query }) => {
|
|
747
|
+
const vecito = vecitoReady ? await vecitoReady : null;
|
|
748
|
+
let results;
|
|
749
|
+
if (vecito) {
|
|
750
|
+
const hits = await vecito.search(query, { top: 15, filter: m => m.type === 'component' });
|
|
751
|
+
results = hits.map(h => h.metadata);
|
|
752
|
+
} else {
|
|
753
|
+
const q = query.toLowerCase();
|
|
754
|
+
results = [];
|
|
755
|
+
for (const [cat, items] of Object.entries(categories)) {
|
|
756
|
+
for (const item of items) {
|
|
757
|
+
const impNames = Array.isArray(item.importName) ? item.importName : (item.importName ? [item.importName] : []);
|
|
758
|
+
if (item.name.toLowerCase().includes(q) || item.label.toLowerCase().includes(q) || impNames.some(n => n.toLowerCase().includes(q))) {
|
|
759
|
+
results.push({ category: cat, name: item.name, label: item.label });
|
|
760
|
+
}
|
|
546
761
|
}
|
|
547
762
|
}
|
|
548
763
|
}
|
|
@@ -554,7 +769,7 @@ export default function attachShowcase(server, options = {}) {
|
|
|
554
769
|
: `No components matching "${query}"`,
|
|
555
770
|
}],
|
|
556
771
|
};
|
|
557
|
-
},
|
|
772
|
+
}),
|
|
558
773
|
);
|
|
559
774
|
|
|
560
775
|
// Fetch a single showcase component's metadata + template snippet. Tries
|
|
@@ -562,23 +777,19 @@ export default function attachShowcase(server, options = {}) {
|
|
|
562
777
|
server.tool(
|
|
563
778
|
'get_component',
|
|
564
779
|
'Get a showcase component\'s template code and metadata',
|
|
565
|
-
{ name: z.string().describe('Component name (e.g. "Btn", "BasicPanel")') },
|
|
780
|
+
{ name: z.string().min(1).describe('Component name (e.g. "Btn", "BasicPanel")') },
|
|
566
781
|
readOnly,
|
|
567
|
-
async ({ name }) => {
|
|
782
|
+
safeTool('get_component', async ({ name }) => {
|
|
568
783
|
const q = name.toLowerCase();
|
|
569
784
|
for (const [cat, items] of Object.entries(categories)) {
|
|
570
785
|
const impMatch = (imp) => { const names = Array.isArray(imp) ? imp : (imp ? [imp] : []); return names.some(n => n.toLowerCase() === q); };
|
|
571
786
|
const item = items.find(i => i.name.toLowerCase() === q || i.label.toLowerCase() === q || impMatch(i.importName));
|
|
572
787
|
if (item) {
|
|
573
|
-
|
|
574
|
-
const impNames = Array.isArray(item.importName) ? item.importName : (item.importName ? [item.importName] : []);
|
|
575
|
-
for (const n of impNames) lines.push(`Import: ${n} from '${item.importFrom || 'varmory'}'`);
|
|
576
|
-
if (item.template) lines.push('', '## Template', '```html', item.template, '```');
|
|
577
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
788
|
+
return { content: [{ type: 'text', text: renderComponentMd(item, cat) }] };
|
|
578
789
|
}
|
|
579
790
|
}
|
|
580
|
-
return { content: [{ type: 'text', text: `Component "${name}" not found.` }] };
|
|
581
|
-
},
|
|
791
|
+
return { isError: true, content: [{ type: 'text', text: `Component "${name}" not found.` }] };
|
|
792
|
+
}),
|
|
582
793
|
);
|
|
583
794
|
|
|
584
795
|
// Render a component's full API (props, slots, events, methods) as
|
|
@@ -587,59 +798,15 @@ export default function attachShowcase(server, options = {}) {
|
|
|
587
798
|
server.tool(
|
|
588
799
|
'get_api',
|
|
589
800
|
'Get the API definition (props, slots, events) for a component',
|
|
590
|
-
{ name: z.string().describe('Component name (e.g. "QBtn", "JPanel")') },
|
|
801
|
+
{ name: z.string().min(1).describe('Component name (e.g. "QBtn", "JPanel")') },
|
|
591
802
|
readOnly,
|
|
592
|
-
async ({ name }) => {
|
|
803
|
+
safeTool('get_api', async ({ name }) => {
|
|
593
804
|
const def = definitions[name];
|
|
594
805
|
if (!def) {
|
|
595
|
-
return { content: [{ type: 'text', text: `No API definition found for "${name}". Available: ${
|
|
806
|
+
return { isError: true, content: [{ type: 'text', text: `No API definition found for "${name}". Available: ${defNames.join(', ')}` }] };
|
|
596
807
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (def.slots && Object.keys(def.slots).length) {
|
|
600
|
-
lines.push('', '## Slots');
|
|
601
|
-
for (const [slot, info] of Object.entries(def.slots)) {
|
|
602
|
-
lines.push(`- **${slot}** — ${info.desc || ''}${formatMixinTag(info)}`);
|
|
603
|
-
// Scoped slot props — indented under the slot bullet.
|
|
604
|
-
if (info.scope && Object.keys(info.scope).length) {
|
|
605
|
-
for (const [k, v] of Object.entries(info.scope)) {
|
|
606
|
-
lines.push(` - \`props.${k}\`: ${formatType(v?.type)} — ${v?.desc || ''}`);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (def.events && Object.keys(def.events).length) {
|
|
613
|
-
lines.push('', '## Events');
|
|
614
|
-
for (const [event, info] of Object.entries(def.events)) {
|
|
615
|
-
lines.push(`- **${event}** — ${info.desc || ''}${formatMixinTag(info)}`);
|
|
616
|
-
// Event payload args — one bullet per param.
|
|
617
|
-
if (info.params && Object.keys(info.params).length) {
|
|
618
|
-
for (const [k, v] of Object.entries(info.params)) {
|
|
619
|
-
lines.push(` - \`${k}\`: ${formatType(v?.type)} — ${v?.desc || ''}`);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (def.methods && Object.keys(def.methods).length) {
|
|
626
|
-
lines.push('', '## Methods');
|
|
627
|
-
for (const [method, info] of Object.entries(def.methods)) {
|
|
628
|
-
lines.push(`- **${method}()** — ${info.desc || ''}${formatMixinTag(info)}`);
|
|
629
|
-
// Method params + return — indented under the method bullet.
|
|
630
|
-
if (info.params && Object.keys(info.params).length) {
|
|
631
|
-
for (const [k, v] of Object.entries(info.params)) {
|
|
632
|
-
const req = v?.required ? ' *(required)*' : '';
|
|
633
|
-
lines.push(` - param \`${k}\`${req}: ${formatType(v?.type)} — ${v?.desc || ''}`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
if (info.returns && typeof info.returns === 'object') {
|
|
637
|
-
lines.push(` - returns: ${formatType(info.returns.type)} — ${info.returns.desc || ''}`);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
642
|
-
},
|
|
808
|
+
return { content: [{ type: 'text', text: renderApiMd(name, def) }] };
|
|
809
|
+
}),
|
|
643
810
|
);
|
|
644
811
|
|
|
645
812
|
// Simple full-text search across docs. Returns the first matching line as
|
|
@@ -647,15 +814,29 @@ export default function attachShowcase(server, options = {}) {
|
|
|
647
814
|
server.tool(
|
|
648
815
|
'search_docs',
|
|
649
816
|
'Search documentation pages by name or content',
|
|
650
|
-
{ query: z.string().describe('Search term to match against doc names and content') },
|
|
817
|
+
{ query: z.string().min(1).describe('Search term to match against doc names and content') },
|
|
651
818
|
readOnly,
|
|
652
|
-
async ({ query }) => {
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
819
|
+
safeTool('search_docs', async ({ query }) => {
|
|
820
|
+
const vecito = vecitoReady ? await vecitoReady : null;
|
|
821
|
+
let results;
|
|
822
|
+
if (vecito) {
|
|
823
|
+
const { Highlighter } = await import('vecito');
|
|
824
|
+
const hits = await vecito.search(query, { top: 5, filter: m => m.type === 'doc', matchedTerms: true });
|
|
825
|
+
results = hits.map(h => {
|
|
826
|
+
const content = docs[h.metadata.name] || '';
|
|
827
|
+
const snippet = h.matchedTerms
|
|
828
|
+
? Highlighter.snippet(content, h.matchedTerms)
|
|
829
|
+
: content.slice(0, 120);
|
|
830
|
+
return { name: h.metadata.name, snippet: snippet.slice(0, 120) };
|
|
831
|
+
});
|
|
832
|
+
} else {
|
|
833
|
+
const q = query.toLowerCase();
|
|
834
|
+
results = [];
|
|
835
|
+
for (const [name, content] of Object.entries(docs)) {
|
|
836
|
+
if (name.toLowerCase().includes(q) || content.toLowerCase().includes(q)) {
|
|
837
|
+
const snippet = content.split('\n').find(l => l.toLowerCase().includes(q))?.trim() || '';
|
|
838
|
+
results.push({ name, snippet: snippet.slice(0, 120) });
|
|
839
|
+
}
|
|
659
840
|
}
|
|
660
841
|
}
|
|
661
842
|
return {
|
|
@@ -666,23 +847,144 @@ export default function attachShowcase(server, options = {}) {
|
|
|
666
847
|
: `No docs matching "${query}"`,
|
|
667
848
|
}],
|
|
668
849
|
};
|
|
669
|
-
},
|
|
850
|
+
}),
|
|
670
851
|
);
|
|
671
852
|
|
|
672
853
|
// Return the full raw markdown of a single doc.
|
|
673
854
|
server.tool(
|
|
674
855
|
'get_doc',
|
|
675
856
|
'Read a documentation page by name',
|
|
676
|
-
{ name: z.string().describe('Doc page name (e.g. "README", "THEMING", "SHOWCASE")') },
|
|
857
|
+
{ name: z.string().min(1).describe('Doc page name (e.g. "README", "THEMING", "SHOWCASE")') },
|
|
677
858
|
readOnly,
|
|
678
|
-
async ({ name }) => {
|
|
859
|
+
safeTool('get_doc', async ({ name }) => {
|
|
679
860
|
const content = docs[name];
|
|
680
861
|
if (!content) {
|
|
681
|
-
return { content: [{ type: 'text', text: `Doc "${name}" not found. Available: ${
|
|
862
|
+
return { isError: true, content: [{ type: 'text', text: `Doc "${name}" not found. Available: ${docNames.join(', ')}` }] };
|
|
682
863
|
}
|
|
683
|
-
return { content: [{ type: 'text', text: content }] };
|
|
684
|
-
},
|
|
864
|
+
return { content: [{ type: 'text', text: serveDoc(content) }] };
|
|
865
|
+
}),
|
|
685
866
|
);
|
|
686
867
|
|
|
687
868
|
return server;
|
|
688
869
|
}
|
|
870
|
+
|
|
871
|
+
// ── Static export ──────────────────────────────────────────────────────────
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Export the docs, components, and API definitions as standalone markdown
|
|
875
|
+
* files under `outDir`. Useful for publishing a browsable snapshot of the
|
|
876
|
+
* showcase alongside (or instead of) the live MCP server.
|
|
877
|
+
*
|
|
878
|
+
* Output layout:
|
|
879
|
+
* <outDir>/README.md (only when a source README.md exists;
|
|
880
|
+
* the original content plus an
|
|
881
|
+
* appended index of everything below)
|
|
882
|
+
* <outDir>/docs/<name>.md (each markdown doc, optionally
|
|
883
|
+
* enriched with showcase templates)
|
|
884
|
+
* <outDir>/components/<Cat>/<Name>.md (each .vue showcase rendered)
|
|
885
|
+
* <outDir>/definitions/<Name>.md (each API definition rendered)
|
|
886
|
+
*
|
|
887
|
+
* @param {object} options
|
|
888
|
+
* @param {string} options.rootDir - varmory project root (required).
|
|
889
|
+
* @param {string} [options.outDir] - Target directory. Default: `<rootDir>/dist/docs`.
|
|
890
|
+
* @param {boolean} [options.quasar=true] - Include Quasar's component JSONs.
|
|
891
|
+
* @param {number} [options.maxDepth=5] - Search depth for categories/definitions folders.
|
|
892
|
+
* @param {boolean} [options.inlineShowcaseExamples=true] - Enrich docs with referenced showcase templates.
|
|
893
|
+
* @param {boolean} [options.clean=true] - Remove `outDir` before writing.
|
|
894
|
+
* Only permitted when `outDir` resolves inside `rootDir`; otherwise throws.
|
|
895
|
+
* @returns {{ outDir: string, docs: number, components: number, definitions: number, cleaned: boolean }}
|
|
896
|
+
*/
|
|
897
|
+
export function exportShowcaseDocs(options = {}) {
|
|
898
|
+
const {
|
|
899
|
+
rootDir,
|
|
900
|
+
quasar = true,
|
|
901
|
+
maxDepth = 5,
|
|
902
|
+
inlineShowcaseExamples = true,
|
|
903
|
+
clean = true,
|
|
904
|
+
} = options;
|
|
905
|
+
if (!rootDir) throw new Error('exportShowcaseDocs: `rootDir` is required');
|
|
906
|
+
|
|
907
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
908
|
+
const resolvedOut = path.resolve(options.outDir || path.join(resolvedRoot, 'dist/docs'));
|
|
909
|
+
|
|
910
|
+
let cleaned = false;
|
|
911
|
+
if (clean) {
|
|
912
|
+
// Safety belt: refuse to clean a path that sits outside rootDir so a
|
|
913
|
+
// misconfigured outDir can't wipe unintended directories.
|
|
914
|
+
const rel = path.relative(resolvedRoot, resolvedOut);
|
|
915
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
916
|
+
throw new Error(
|
|
917
|
+
`exportShowcaseDocs: refusing to clean outDir (${resolvedOut}) — must be inside rootDir (${resolvedRoot}). Pass clean: false to skip.`,
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
fs.rmSync(resolvedOut, { recursive: true, force: true });
|
|
921
|
+
cleaned = true;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const { categories, docs, definitions } = loadAll({ rootDir: resolvedRoot, quasar, maxDepth });
|
|
925
|
+
|
|
926
|
+
fs.mkdirSync(resolvedOut, { recursive: true });
|
|
927
|
+
const docsOut = path.join(resolvedOut, 'docs');
|
|
928
|
+
const componentsOut = path.join(resolvedOut, 'components');
|
|
929
|
+
const definitionsOut = path.join(resolvedOut, 'definitions');
|
|
930
|
+
|
|
931
|
+
// Docs. README is skipped here — it becomes the top-level index below.
|
|
932
|
+
let docCount = 0;
|
|
933
|
+
const docNames = Object.keys(docs).filter(n => n !== 'README').sort();
|
|
934
|
+
if (docNames.length) fs.mkdirSync(docsOut, { recursive: true });
|
|
935
|
+
for (const name of docNames) {
|
|
936
|
+
const body = inlineShowcaseExamples
|
|
937
|
+
? enrichDocWithShowcaseLinks(docs[name], categories)
|
|
938
|
+
: docs[name];
|
|
939
|
+
fs.writeFileSync(path.join(docsOut, `${name}.md`), body);
|
|
940
|
+
docCount++;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Components.
|
|
944
|
+
let componentCount = 0;
|
|
945
|
+
const catNames = Object.keys(categories).sort();
|
|
946
|
+
if (catNames.length) fs.mkdirSync(componentsOut, { recursive: true });
|
|
947
|
+
for (const cat of catNames) {
|
|
948
|
+
const catDir = path.join(componentsOut, cat);
|
|
949
|
+
fs.mkdirSync(catDir, { recursive: true });
|
|
950
|
+
for (const item of categories[cat]) {
|
|
951
|
+
fs.writeFileSync(path.join(catDir, `${item.name}.md`), renderComponentMd(item, cat));
|
|
952
|
+
componentCount++;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// API definitions.
|
|
957
|
+
let definitionCount = 0;
|
|
958
|
+
const defNames = Object.keys(definitions).sort();
|
|
959
|
+
if (defNames.length) fs.mkdirSync(definitionsOut, { recursive: true });
|
|
960
|
+
for (const name of defNames) {
|
|
961
|
+
fs.writeFileSync(path.join(definitionsOut, `${name}.md`), renderApiMd(name, definitions[name]));
|
|
962
|
+
definitionCount++;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Index README (only when source README exists).
|
|
966
|
+
if (docs['README']) {
|
|
967
|
+
const parts = [docs['README'].trimEnd(), '', '## Docs', ''];
|
|
968
|
+
for (const name of docNames) parts.push(`- [${name}](docs/${name}.md)`);
|
|
969
|
+
parts.push('', '## Components', '');
|
|
970
|
+
for (const cat of catNames) {
|
|
971
|
+
parts.push(`### ${cat}`, '');
|
|
972
|
+
for (const item of categories[cat]) {
|
|
973
|
+
parts.push(`- [${item.label}](components/${cat}/${item.name}.md)`);
|
|
974
|
+
}
|
|
975
|
+
parts.push('');
|
|
976
|
+
}
|
|
977
|
+
parts.push('## API definitions', '');
|
|
978
|
+
for (const name of defNames) parts.push(`- [${name}](definitions/${name}.md)`);
|
|
979
|
+
parts.push('');
|
|
980
|
+
fs.writeFileSync(path.join(resolvedOut, 'README.md'), parts.join('\n'));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return {
|
|
984
|
+
outDir: resolvedOut,
|
|
985
|
+
docs: docCount,
|
|
986
|
+
components: componentCount,
|
|
987
|
+
definitions: definitionCount,
|
|
988
|
+
cleaned,
|
|
989
|
+
};
|
|
990
|
+
}
|