@morya-ui/mcp 0.1.1

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/dist/tools.js ADDED
@@ -0,0 +1,975 @@
1
+ import { findComponent, findGuide, loadCatalog, normalizeName, resolveLocale, textResult, toKebab, } from './catalog.js';
2
+ import { componentDecisions, findDecision, scoreDecision } from './decisions.js';
3
+ import { designRules, findPattern, pagePatterns, scorePattern } from './patterns.js';
4
+ import { countCatalogResourceTemplates, countCatalogResources } from './resources.js';
5
+ function inspectButtonIconOnlyUsage(code, issues) {
6
+ const pairedTagRe = /<MButton\b([^>]*)>([\s\S]*?)<\/MButton>/gi;
7
+ let match = pairedTagRe.exec(code);
8
+ while (match !== null) {
9
+ const attrs = match[1] || '';
10
+ const inner = (match[2] || '').replace(/<!--[\s\S]*?-->/g, '').trim();
11
+ const hasIconOnly = /\b(?:icon-only|iconOnly)\b/.test(attrs);
12
+ const hasIconProp = /\b(?::icon|icon=)/.test(attrs);
13
+ if (hasIconOnly && !hasIconProp) {
14
+ issues.push({
15
+ type: 'icon-only-missing-icon',
16
+ message: 'MButton with icon-only must set icon (or :icon). Default slot content is not rendered when iconOnly is true.',
17
+ });
18
+ }
19
+ if (hasIconOnly && inner.length > 0) {
20
+ issues.push({
21
+ type: 'icon-only-default-slot',
22
+ message: 'MButton with icon-only ignores default slot content. Pass the icon via icon / :icon instead.',
23
+ });
24
+ }
25
+ match = pairedTagRe.exec(code);
26
+ }
27
+ const selfClosingRe = /<MButton\b([^>]*)\/>/gi;
28
+ match = selfClosingRe.exec(code);
29
+ while (match !== null) {
30
+ const attrs = match[1] || '';
31
+ const hasIconOnly = /\b(?:icon-only|iconOnly)\b/.test(attrs);
32
+ const hasIconProp = /\b(?::icon|icon=)/.test(attrs);
33
+ if (hasIconOnly && !hasIconProp) {
34
+ issues.push({
35
+ type: 'icon-only-missing-icon',
36
+ message: 'MButton with icon-only must set icon (or :icon).',
37
+ });
38
+ }
39
+ match = selfClosingRe.exec(code);
40
+ }
41
+ }
42
+ function pickLocale(record, locale) {
43
+ return record.locales[locale] || record.locales['zh-CN'] || record.locales['en-US'] || null;
44
+ }
45
+ function vueName(name) {
46
+ return `M${name}`;
47
+ }
48
+ function generatedPageCode(patternId, intent, locale) {
49
+ const zh = locale === 'zh-CN';
50
+ const isList = patternId === 'admin-list';
51
+ const isDashboard = patternId === 'dashboard';
52
+ const isDetail = patternId === 'detail-page';
53
+ const isEmpty = patternId === 'empty-state';
54
+ const isWizard = patternId === 'wizard-form';
55
+ const isSettings = patternId === 'settings-page';
56
+ const isAuth = patternId === 'auth-page';
57
+ const isForm = patternId === 'form-page' || isSettings;
58
+ const useLayoutShell = isList || isForm || isDashboard || isDetail || isSettings;
59
+ const title = intent || (zh ? '业务页面' : 'Business page');
60
+ const layoutImports = useLayoutShell
61
+ ? ', MLayout, MLayoutContent, MLayoutHeader, MLayoutSider, MBreadcrumb'
62
+ : '';
63
+ const listImports = isList ? ', MSelect, MSpace, MTable' : '';
64
+ const formImports = isForm || isAuth || isWizard ? ', MForm, MFormItem, MSelect' : '';
65
+ const dashboardImports = isDashboard ? ', MCard, MGrid, MGridItem, MSkeleton, MTable' : '';
66
+ const detailImports = isDetail ? ', MDivider' : '';
67
+ const emptyImports = isEmpty ? ', MDataView' : '';
68
+ const wizardImports = isWizard ? ', MStepper' : '';
69
+ const settingsImports = isSettings ? ', MTabs' : '';
70
+ const script = `<script setup lang="ts">
71
+ import { ref } from 'vue'
72
+ import { MButton, MCard, MConfigProvider, MInput, MTag, zhCN${layoutImports}${listImports}${formImports}${dashboardImports}${detailImports}${emptyImports}${wizardImports}${settingsImports} } from 'morya-ui'
73
+
74
+ const loading = ref(false)
75
+ const error = ref('')
76
+ ${isList ? `const keyword = ref('')
77
+ const rows = ref<Record<string, unknown>[]>([])
78
+ const columns = [{ key: 'name', label: '${zh ? '名称' : 'Name'}' }, { key: 'status', label: '${zh ? '状态' : 'Status'}' }]
79
+ ` : ''}${isForm || isAuth || isWizard ? `const model = ref({ name: '' })
80
+ ` : ''}${isDashboard ? `const metrics = ref([
81
+ { label: '${zh ? '总用户' : 'Users'}', value: '0' },
82
+ { label: '${zh ? '今日活跃' : 'Active today'}', value: '0' },
83
+ ])
84
+ ` : ''}${isWizard ? `const activeStep = ref(0)
85
+ ` : ''}
86
+
87
+ async function submit() {
88
+ loading.value = true
89
+ error.value = ''
90
+ try {
91
+ // Replace with the page API request.
92
+ } finally {
93
+ loading.value = false
94
+ }
95
+ }
96
+ </script>`;
97
+ const listContent = ` <section class="m-generated-filters" aria-label="${zh ? '筛选' : 'Filters'}">
98
+ <MSpace wrap>
99
+ <MInput v-model="keyword" placeholder="${zh ? '搜索关键词' : 'Search keyword'}" clearable style="width: 14rem" />
100
+ <MButton severity="primary">${zh ? '查询' : 'Search'}</MButton>
101
+ <MButton severity="secondary">${zh ? '重置' : 'Reset'}</MButton>
102
+ </MSpace>
103
+ </section>
104
+ <header class="m-generated-toolbar">
105
+ <h1 class="m-generated-title">${title}</h1>
106
+ <MButton severity="primary">${zh ? '新建' : 'Create'}</MButton>
107
+ </header>
108
+ <MTable :columns="columns" :rows="rows" :loading="loading" paginator :rows-per-page="10" striped bordered row-key="id">
109
+ <template #empty>
110
+ <p class="m-generated-muted">${zh ? '暂无数据' : 'No data yet'}</p>
111
+ </template>
112
+ </MTable>`;
113
+ const formContent = ` <header class="m-generated-intro">
114
+ <h1 class="m-generated-title">${title}</h1>
115
+ <p class="m-generated-muted">${zh ? '填写表单并保存。' : 'Fill in the form and save.'}</p>
116
+ </header>
117
+ <MForm class="m-generated-form" @submit.prevent="submit">
118
+ <MFormItem label="${zh ? '名称' : 'Name'}" name="name" required>
119
+ <MInput v-model="model.name" fluid />
120
+ </MFormItem>
121
+ <footer class="m-generated-actions">
122
+ <MButton native-type="submit" severity="primary" :loading="loading">${zh ? '保存' : 'Save'}</MButton>
123
+ <MButton severity="secondary">${zh ? '取消' : 'Cancel'}</MButton>
124
+ </footer>
125
+ </MForm>`;
126
+ const dashboardContent = ` <h1 class="m-generated-title">${title}</h1>
127
+ <MGrid :cols="2" :x-gap="16" :y-gap="16" responsive="screen">
128
+ <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
129
+ <MCard>
130
+ <p class="m-generated-muted">{{ metric.label }}</p>
131
+ <strong class="m-generated-metric">{{ metric.value }}</strong>
132
+ </MCard>
133
+ </MGridItem>
134
+ </MGrid>
135
+ <MCard :title="${zh ? '趋势概览' : 'Trend overview'}">
136
+ <MSkeleton v-if="loading" height="8rem" />
137
+ <p v-else class="m-generated-muted">${zh ? '接入图表或业务组件。' : 'Connect charts or business widgets here.'}</p>
138
+ </MCard>`;
139
+ const detailContent = ` <header class="m-generated-toolbar">
140
+ <div>
141
+ <h1 class="m-generated-title">${title}</h1>
142
+ <MTag value="${zh ? '正常' : 'Active'}" severity="success" />
143
+ </div>
144
+ <MButton severity="primary" outlined>${zh ? '编辑' : 'Edit'}</MButton>
145
+ </header>
146
+ <MCard>
147
+ <MDivider />
148
+ <dl class="m-generated-details">
149
+ <div><dt>${zh ? '名称' : 'Name'}</dt><dd>${zh ? '示例资源' : 'Example resource'}</dd></div>
150
+ <div><dt>${zh ? '更新时间' : 'Updated'}</dt><dd>—</dd></div>
151
+ </dl>
152
+ </MCard>`;
153
+ const settingsContent = ` <h1 class="m-generated-title">${title}</h1>
154
+ <MTabs :value="'general'" :items="[{ label: '${zh ? '常规' : 'General'}', value: 'general' }]" />
155
+ <MForm class="m-generated-form" @submit.prevent="submit">
156
+ <MFormItem label="${zh ? '显示名称' : 'Display name'}" name="name">
157
+ <MInput v-model="model.name" fluid />
158
+ </MFormItem>
159
+ <MButton native-type="submit" severity="primary" :loading="loading">${zh ? '保存设置' : 'Save settings'}</MButton>
160
+ </MForm>`;
161
+ let innerTemplate = '';
162
+ if (isList) {
163
+ innerTemplate = `<MConfigProvider :locale="zhCN">
164
+ <MLayout has-sider class="m-generated-page">
165
+ <MLayoutSider class="m-generated-sider" />
166
+ <MLayout>
167
+ <MLayoutHeader class="m-generated-header">
168
+ <MBreadcrumb :model="[{ label: '${zh ? '首页' : 'Home'}', to: '/' }, { label: '${title}' }]" />
169
+ </MLayoutHeader>
170
+ <MLayoutContent class="m-generated-content">
171
+ ${listContent}
172
+ </MLayoutContent>
173
+ </MLayout>
174
+ </MLayout>
175
+ </MConfigProvider>`;
176
+ }
177
+ else if (useLayoutShell) {
178
+ const content = isDashboard ? dashboardContent : isDetail ? detailContent : isSettings ? settingsContent : formContent;
179
+ innerTemplate = `<MConfigProvider :locale="zhCN">
180
+ <MLayout class="m-generated-page">
181
+ <MLayoutHeader class="m-generated-header">
182
+ <MBreadcrumb :model="[{ label: '${zh ? '首页' : 'Home'}', to: '/' }, { label: '${title}' }]" />
183
+ </MLayoutHeader>
184
+ <MLayoutContent class="m-generated-content">
185
+ ${content}
186
+ </MLayoutContent>
187
+ </MLayout>
188
+ </MConfigProvider>`;
189
+ }
190
+ else if (isAuth) {
191
+ innerTemplate = `<MConfigProvider :locale="zhCN">
192
+ <main class="m-generated-page m-generated-auth">
193
+ <MCard>
194
+ <MForm label-position="top" @submit.prevent="submit">
195
+ <MFormItem label="${zh ? '邮箱' : 'Email'}" name="email">
196
+ <MInput type="email" fluid />
197
+ </MFormItem>
198
+ <MFormItem label="${zh ? '密码' : 'Password'}" name="password">
199
+ <MInput type="password" fluid />
200
+ </MFormItem>
201
+ <MButton native-type="submit" severity="primary" :loading="loading" fluid>${zh ? '登录' : 'Sign in'}</MButton>
202
+ </MForm>
203
+ </MCard>
204
+ </main>
205
+ </MConfigProvider>`;
206
+ }
207
+ else if (isEmpty) {
208
+ innerTemplate = `<MConfigProvider :locale="zhCN">
209
+ <main class="m-generated-page">
210
+ <MCard>
211
+ <MDataView :value="[]">
212
+ <template #empty>
213
+ <div class="m-generated-empty">
214
+ <strong>${zh ? '暂无内容' : 'Nothing here yet'}</strong>
215
+ <p class="m-generated-muted">${zh ? '创建第一条记录开始使用。' : 'Create your first record to get started.'}</p>
216
+ <MButton severity="primary" @click="submit">${zh ? '创建' : 'Create'}</MButton>
217
+ </div>
218
+ </template>
219
+ </MDataView>
220
+ </MCard>
221
+ </main>
222
+ </MConfigProvider>`;
223
+ }
224
+ else if (isWizard) {
225
+ innerTemplate = `<MConfigProvider :locale="zhCN">
226
+ <main class="m-generated-page">
227
+ <MCard>
228
+ <MStepper v-model="activeStep" :items="[${zh ? "'基本信息', '确认'" : "'Details', 'Confirm'"}]" />
229
+ <MForm label-position="top" @submit.prevent="submit">
230
+ <MFormItem label="${zh ? '名称' : 'Name'}" name="name"><MInput v-model="model.name" fluid /></MFormItem>
231
+ <MButton native-type="submit" severity="primary" :loading="loading">${zh ? '下一步' : 'Next'}</MButton>
232
+ </MForm>
233
+ </MCard>
234
+ </main>
235
+ </MConfigProvider>`;
236
+ }
237
+ else {
238
+ innerTemplate = `<MConfigProvider :locale="zhCN">
239
+ <main class="m-generated-page">
240
+ <MCard>
241
+ <p class="m-generated-muted">${zh ? '将此区域替换为页面内容。' : 'Replace this area with page content.'}</p>
242
+ <MTag value="${zh ? '示例' : 'Example'}" severity="info" />
243
+ </MCard>
244
+ </main>
245
+ </MConfigProvider>`;
246
+ }
247
+ const template = `<template>
248
+ ${innerTemplate}
249
+ <p v-if="error" role="alert" class="m-generated-error">${'{{ error }}'}</p>
250
+ </template>`;
251
+ const style = `<style scoped>
252
+ .m-generated-page { min-height: 100vh; background: var(--m-color-surface); }
253
+ .m-generated-sider { border-right: 1px solid var(--m-color-border); }
254
+ .m-generated-header { padding: var(--m-space-4) var(--m-space-6); border-bottom: 1px solid var(--m-color-border); }
255
+ .m-generated-content { padding: var(--m-space-6); display: flex; flex-direction: column; gap: var(--m-space-4); }
256
+ .m-generated-filters { padding: var(--m-space-4); background: color-mix(in srgb, var(--m-color-border) 25%, transparent); border-radius: var(--m-radius-md); border: 1px solid var(--m-color-border); }
257
+ .m-generated-toolbar, .m-generated-actions { display: flex; gap: var(--m-space-3); align-items: center; justify-content: space-between; flex-wrap: wrap; }
258
+ .m-generated-title { margin: 0; font-size: var(--m-font-size-lg); font-weight: 600; color: var(--m-color-text); }
259
+ .m-generated-intro { margin-bottom: var(--m-space-2); }
260
+ .m-generated-form { padding: var(--m-space-6); border: 1px solid var(--m-color-border); border-radius: var(--m-radius-md); box-shadow: var(--m-shadow-sm); }
261
+ .m-generated-auth { display: grid; place-items: center; padding: var(--m-space-8); max-width: 24rem; margin: 0 auto; }
262
+ .m-generated-metric { display: block; font-size: var(--m-font-size-lg); margin: var(--m-space-2) 0; }
263
+ .m-generated-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--m-space-4); margin: 0; }
264
+ .m-generated-details dt { color: var(--m-color-text-muted); font-size: var(--m-font-size-sm); }
265
+ .m-generated-details dd { margin: var(--m-space-1) 0 0; }
266
+ .m-generated-empty { display: grid; gap: var(--m-space-2); justify-items: center; padding: var(--m-space-8); text-align: center; }
267
+ .m-generated-muted { margin: 0; color: var(--m-color-text-muted); }
268
+ .m-generated-error { color: var(--m-color-danger); padding: 0 var(--m-space-6); }
269
+ @media (max-width: 48rem) { .m-generated-content { padding: var(--m-space-4); } .m-generated-details { grid-template-columns: 1fr; } }
270
+ </style>`;
271
+ return { script, template, style };
272
+ }
273
+ function scoreMatch(haystack, query) {
274
+ const text = haystack.toLowerCase();
275
+ const q = query.toLowerCase();
276
+ if (!q)
277
+ return 0;
278
+ if (text === q)
279
+ return 100;
280
+ if (text.startsWith(q))
281
+ return 80;
282
+ if (text.includes(q))
283
+ return 50;
284
+ const parts = q.split(/\s+/).filter(Boolean);
285
+ let score = 0;
286
+ for (const part of parts) {
287
+ if (text.includes(part))
288
+ score += 20;
289
+ }
290
+ return score;
291
+ }
292
+ function componentSearchBlob(component) {
293
+ return [
294
+ component.id,
295
+ component.name,
296
+ component.exportName,
297
+ component.category,
298
+ component.description,
299
+ component.descriptionEn,
300
+ ...component.props.map((item) => `${item.name} ${item.description}`),
301
+ ...component.examples.map((item) => `${item.section} ${item.code}`),
302
+ ].join('\n');
303
+ }
304
+ function pagination(total, offset, limit) {
305
+ const nextOffset = offset + limit;
306
+ return {
307
+ total,
308
+ count: Math.max(Math.min(limit, total - offset), 0),
309
+ offset,
310
+ limit,
311
+ has_more: nextOffset < total,
312
+ ...(nextOffset < total ? { next_offset: nextOffset } : {}),
313
+ };
314
+ }
315
+ function apiCoverage(component, examples) {
316
+ const source = examples.map((example) => example.code).join('\n');
317
+ const has = (name) => {
318
+ const kebab = toKebab(name);
319
+ return new RegExp(`(?:^|[\\s:@])(?:${name}|${kebab})(?:[\\s=/>]|$)`, 'i').test(source) ||
320
+ (name === 'modelValue' && /v-model(?:[:=]|\\s)/i.test(source));
321
+ };
322
+ const eventHas = (name) => source.includes(`@${name}`) || source.includes(`@${toKebab(name)}`);
323
+ const slotHas = (name) => source.includes(`#${name}`) ||
324
+ (name === 'default' && source.includes('<M') && source.includes('</M>'));
325
+ const summary = (items, predicate) => ({
326
+ total: items.length,
327
+ covered: items.filter(predicate).length,
328
+ missing: items.filter((item) => !predicate(item)),
329
+ });
330
+ return {
331
+ props: summary(component.props.map((item) => item.name), has),
332
+ events: summary(component.events.map((item) => item.name), eventHas),
333
+ slots: summary(component.slots.map((item) => item.name), slotHas),
334
+ methods: summary((component.methods || []).map((item) => item.name), (name) => new RegExp(`(?:\\.|ref\\?\\.)${name}\\s*\\(`).test(source) || source.includes(`\`${name}\``)),
335
+ };
336
+ }
337
+ function guideSearchBlob(guide) {
338
+ return [
339
+ guide.id,
340
+ guide.title,
341
+ guide.titleEn,
342
+ guide.description,
343
+ guide.descriptionEn,
344
+ guide.locales['zh-CN']?.markdown || '',
345
+ guide.locales['en-US']?.markdown || '',
346
+ ].join('\n');
347
+ }
348
+ export function createToolHandlers(catalog = loadCatalog()) {
349
+ function list(args) {
350
+ const kind = args.kind || 'components';
351
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
352
+ const offset = Math.max(args.offset ?? 0, 0);
353
+ const locale = resolveLocale(args.mode);
354
+ if (kind === 'guides') {
355
+ const items = catalog.guides.slice(offset, offset + limit).map((guide) => {
356
+ const local = pickLocale(guide, locale);
357
+ return {
358
+ id: guide.id,
359
+ title: local?.title || guide.title,
360
+ description: local?.description || guide.description,
361
+ order: guide.order,
362
+ };
363
+ });
364
+ return textResult({ kind, ...pagination(catalog.guides.length, offset, limit), items });
365
+ }
366
+ if (kind === 'patterns')
367
+ return listPatterns(args);
368
+ if (kind === 'categories') {
369
+ const counts = new Map();
370
+ for (const component of catalog.components) {
371
+ const key = component.category || 'Uncategorized';
372
+ counts.set(key, (counts.get(key) || 0) + 1);
373
+ }
374
+ const allItems = [...counts.entries()]
375
+ .map(([category, count]) => ({ category, count }))
376
+ .sort((a, b) => a.category.localeCompare(b.category));
377
+ const items = allItems.slice(offset, offset + limit);
378
+ return textResult({ kind, ...pagination(allItems.length, offset, limit), items });
379
+ }
380
+ if (kind === 'examples') {
381
+ const flat = catalog.components.flatMap((component) => component.examples
382
+ .filter((example) => !args.mode || example.locale === locale)
383
+ .map((example) => ({
384
+ component: component.id,
385
+ exportName: component.exportName,
386
+ ...example,
387
+ })));
388
+ return textResult({
389
+ kind,
390
+ ...pagination(flat.length, offset, limit),
391
+ items: flat.slice(offset, offset + limit),
392
+ });
393
+ }
394
+ const items = catalog.components.slice(offset, offset + limit).map((component) => ({
395
+ id: component.id,
396
+ exportName: component.exportName,
397
+ category: component.category,
398
+ description: locale === 'en-US'
399
+ ? component.descriptionEn || component.description
400
+ : component.description || component.descriptionEn,
401
+ }));
402
+ return textResult({ kind: 'components', ...pagination(catalog.components.length, offset, limit), items });
403
+ }
404
+ function search(args) {
405
+ const query = args.query.trim();
406
+ const scope = args.scope || 'all';
407
+ const limit = Math.min(Math.max(args.limit ?? 10, 1), 50);
408
+ const offset = Math.max(args.offset ?? 0, 0);
409
+ const locale = resolveLocale(args.mode);
410
+ const hits = [];
411
+ if (scope === 'all' || scope === 'components' || scope === 'api' || scope === 'examples') {
412
+ for (const component of catalog.components) {
413
+ let score = scoreMatch(componentSearchBlob(component), query);
414
+ if (scope === 'api') {
415
+ score = Math.max(...component.props.map((prop) => scoreMatch(`${prop.name} ${prop.type} ${prop.description}`, query)), 0);
416
+ }
417
+ if (scope === 'examples') {
418
+ score = Math.max(...component.examples.map((example) => scoreMatch(`${example.section} ${example.code}`, query)), 0);
419
+ }
420
+ if (score > 0) {
421
+ hits.push({
422
+ type: 'component',
423
+ id: component.id,
424
+ title: component.exportName,
425
+ score,
426
+ snippet: locale === 'en-US'
427
+ ? component.descriptionEn || component.description
428
+ : component.description,
429
+ });
430
+ }
431
+ }
432
+ }
433
+ if (scope === 'all' || scope === 'patterns') {
434
+ for (const pattern of pagePatterns) {
435
+ const score = scorePattern(pattern, query);
436
+ if (score > 0) {
437
+ const title = locale === 'en-US' ? pattern.titleEn : pattern.title;
438
+ hits.push({
439
+ type: 'pattern',
440
+ id: pattern.id,
441
+ title,
442
+ score,
443
+ snippet: locale === 'en-US' ? pattern.descriptionEn : pattern.description,
444
+ });
445
+ }
446
+ }
447
+ }
448
+ if (scope === 'all' || scope === 'decisions') {
449
+ for (const decision of componentDecisions) {
450
+ const score = scoreDecision(decision, query);
451
+ if (score > 0) {
452
+ hits.push({
453
+ type: 'decision',
454
+ id: decision.id,
455
+ title: locale === 'en-US' ? decision.titleEn : decision.title,
456
+ score,
457
+ snippet: locale === 'en-US' ? decision.questionEn : decision.question,
458
+ });
459
+ }
460
+ }
461
+ }
462
+ if (scope === 'all' || scope === 'guides') {
463
+ for (const guide of catalog.guides) {
464
+ const score = scoreMatch(guideSearchBlob(guide), query);
465
+ if (score > 0) {
466
+ const local = pickLocale(guide, locale);
467
+ hits.push({
468
+ type: 'guide',
469
+ id: guide.id,
470
+ title: local?.title || guide.title,
471
+ score,
472
+ snippet: local?.description || guide.description,
473
+ });
474
+ }
475
+ }
476
+ }
477
+ hits.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
478
+ return textResult({
479
+ query,
480
+ scope,
481
+ ...pagination(hits.length, offset, limit),
482
+ items: hits.slice(offset, offset + limit),
483
+ });
484
+ }
485
+ function getComponent(args) {
486
+ const names = [
487
+ ...(args.component ? [args.component] : []),
488
+ ...(args.components || []).filter(Boolean),
489
+ ].slice(0, 10);
490
+ if (names.length === 0) {
491
+ return textResult({ error: 'Provide component or components.' });
492
+ }
493
+ const locale = resolveLocale(args.mode);
494
+ const detail = args.detail || 'compact';
495
+ const includeApi = args.includeApi ?? detail === 'full';
496
+ const includeExamples = args.includeExamples ?? detail === 'full';
497
+ const items = names.map((name) => {
498
+ const component = findComponent(catalog, name);
499
+ if (!component)
500
+ return { query: name, error: `Component not found: ${name}` };
501
+ const local = pickLocale(component, locale);
502
+ const selectedSections = (args.sections || [])
503
+ .map((section) => normalizeName(section))
504
+ .filter(Boolean);
505
+ const sections = local?.sections?.filter((section) => {
506
+ if (selectedSections.length === 0)
507
+ return detail === 'full';
508
+ return selectedSections.includes(normalizeName(section.id)) ||
509
+ selectedSections.includes(normalizeName(section.title));
510
+ }) || [];
511
+ const localeExamples = component.examples.filter((example) => example.locale === locale);
512
+ const examplesLimit = Math.min(Math.max(args.examplesLimit ?? 8, 1), 100);
513
+ const examplesOffset = Math.max(args.examplesOffset ?? 0, 0);
514
+ const examplePage = localeExamples.slice(examplesOffset, examplesOffset + examplesLimit);
515
+ return {
516
+ id: component.id,
517
+ exportName: component.exportName,
518
+ category: component.category,
519
+ description: local?.description || component.description,
520
+ import: component.import,
521
+ ...(includeApi
522
+ ? {
523
+ props: component.props,
524
+ events: component.events,
525
+ slots: component.slots,
526
+ methods: component.methods || [],
527
+ apiCoverage: apiCoverage(component, localeExamples),
528
+ }
529
+ : {}),
530
+ ...(includeExamples
531
+ ? {
532
+ examples: examplePage,
533
+ exampleCount: localeExamples.length,
534
+ examplesOffset,
535
+ examplesLimit,
536
+ hasMoreExamples: examplesOffset + examplesLimit < localeExamples.length,
537
+ ...(examplesOffset + examplesLimit < localeExamples.length
538
+ ? { nextExamplesOffset: examplesOffset + examplesLimit }
539
+ : {}),
540
+ }
541
+ : {
542
+ exampleCount: localeExamples.length,
543
+ examplesOffset: 0,
544
+ examplesLimit,
545
+ hasMoreExamples: localeExamples.length > 0,
546
+ }),
547
+ sections: detail === 'full' || selectedSections.length
548
+ ? sections.map((section) => ({
549
+ id: section.id,
550
+ title: section.title,
551
+ body: section.body,
552
+ }))
553
+ : (local?.sections || []).map((section) => section.title).filter(Boolean),
554
+ ...(detail === 'full' ? { markdown: local?.markdown } : {}),
555
+ };
556
+ });
557
+ return textResult(names.length === 1 ? items[0] : { items });
558
+ }
559
+ function getExample(args) {
560
+ const component = findComponent(catalog, args.component);
561
+ if (!component)
562
+ return textResult({ error: `Component not found: ${args.component}` });
563
+ const locale = resolveLocale(args.mode);
564
+ let examples = component.examples.filter((example) => example.locale === locale);
565
+ if (examples.length === 0)
566
+ examples = component.examples;
567
+ if (args.section) {
568
+ const key = normalizeName(args.section);
569
+ examples = examples.filter((example) => normalizeName(example.section) === key || normalizeName(example.sectionId) === key);
570
+ }
571
+ if (args.variant) {
572
+ const key = normalizeName(args.variant);
573
+ examples = examples.filter((example) => normalizeName(example.id) === key || normalizeName(example.section).includes(key));
574
+ }
575
+ if (examples.length === 0) {
576
+ return textResult({
577
+ error: `No example found for ${component.exportName}`,
578
+ availableSections: [...new Set(component.examples.map((item) => item.section))],
579
+ });
580
+ }
581
+ const example = examples[0];
582
+ return textResult({
583
+ component: component.id,
584
+ exportName: component.exportName,
585
+ import: component.import,
586
+ example,
587
+ });
588
+ }
589
+ function getGuide(args) {
590
+ const guide = findGuide(catalog, args.guide);
591
+ if (!guide)
592
+ return textResult({ error: `Guide not found: ${args.guide}` });
593
+ const locale = resolveLocale(args.mode);
594
+ const local = pickLocale(guide, locale);
595
+ const detail = args.detail || 'compact';
596
+ if (args.section && local?.sections) {
597
+ const key = normalizeName(args.section);
598
+ const section = local.sections.find((item) => normalizeName(item.id) === key || normalizeName(item.title) === key);
599
+ if (!section) {
600
+ return textResult({
601
+ error: `Section not found: ${args.section}`,
602
+ availableSections: local.sections.map((item) => item.title).filter(Boolean),
603
+ });
604
+ }
605
+ return textResult({
606
+ id: guide.id,
607
+ title: local.title,
608
+ section,
609
+ });
610
+ }
611
+ return textResult({
612
+ id: guide.id,
613
+ title: local?.title || guide.title,
614
+ description: local?.description || guide.description,
615
+ sections: (local?.sections || []).map((section) => section.title).filter(Boolean),
616
+ ...(detail === 'full' ? { markdown: local?.markdown, bodySections: local?.sections } : {}),
617
+ });
618
+ }
619
+ function listPatterns(args) {
620
+ const locale = resolveLocale(args.mode);
621
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
622
+ const offset = Math.max(args.offset ?? 0, 0);
623
+ const items = pagePatterns.slice(offset, offset + limit).map((pattern) => ({
624
+ id: pattern.id,
625
+ title: locale === 'en-US' ? pattern.titleEn : pattern.title,
626
+ description: locale === 'en-US' ? pattern.descriptionEn : pattern.description,
627
+ keywords: pattern.keywords,
628
+ components: pattern.components.map((item) => item.component),
629
+ }));
630
+ return textResult({ kind: 'patterns', ...pagination(pagePatterns.length, offset, limit), items });
631
+ }
632
+ function getPattern(args) {
633
+ const pattern = findPattern(args.pattern);
634
+ if (!pattern) {
635
+ return textResult({
636
+ error: `Pattern not found: ${args.pattern}`,
637
+ availablePatterns: pagePatterns.map((item) => item.id),
638
+ });
639
+ }
640
+ const locale = resolveLocale(args.mode);
641
+ return textResult({
642
+ id: pattern.id,
643
+ title: locale === 'en-US' ? pattern.titleEn : pattern.title,
644
+ description: locale === 'en-US' ? pattern.descriptionEn : pattern.description,
645
+ keywords: pattern.keywords,
646
+ components: pattern.components,
647
+ structure: pattern.structure,
648
+ layout: pattern.layout,
649
+ styleRules: pattern.styleRules,
650
+ interactionRules: pattern.interactionRules,
651
+ avoid: pattern.avoid,
652
+ });
653
+ }
654
+ function recommendPage(args) {
655
+ const query = [args.intent, args.pageType || '', ...(args.features || [])].join(' ');
656
+ const ranked = pagePatterns
657
+ .map((pattern) => ({ pattern, score: scorePattern(pattern, query) }))
658
+ .sort((a, b) => b.score - a.score || a.pattern.id.localeCompare(b.pattern.id));
659
+ const best = ranked[0];
660
+ if (!best || best.score === 0) {
661
+ return textResult({
662
+ error: 'No page pattern matched the request.',
663
+ suggestions: pagePatterns.map((pattern) => ({ id: pattern.id, title: pattern.title })),
664
+ });
665
+ }
666
+ const locale = resolveLocale(args.mode);
667
+ const result = {
668
+ intent: args.intent,
669
+ pageType: args.pageType,
670
+ matchedPattern: best.pattern.id,
671
+ title: locale === 'en-US' ? best.pattern.titleEn : best.pattern.title,
672
+ confidence: best.score,
673
+ goldenPage: best.pattern.goldenPage,
674
+ components: best.pattern.components,
675
+ structure: best.pattern.structure,
676
+ layout: best.pattern.layout,
677
+ styleRules: best.pattern.styleRules,
678
+ interactionRules: best.pattern.interactionRules,
679
+ avoid: best.pattern.avoid,
680
+ alternatives: ranked.slice(1, 3).filter((item) => item.score > 0).map((item) => ({ id: item.pattern.id, score: item.score })),
681
+ nextStep: locale === 'en-US'
682
+ ? `Read goldenPage (${best.pattern.goldenPage || 'none'}) and matchedPattern with get_pattern, then verify component APIs with get_component or get_example. Pass includeScaffold: true for a starter Vue file aligned with MLayout shell.`
683
+ : `先阅读 goldenPage(${best.pattern.goldenPage || '无'})并用 get_pattern 读取 matchedPattern,再用 get_component 或 get_example 核对组件 API。需要 starter 代码时传 includeScaffold: true(已对齐 MLayout 骨架)。`,
684
+ };
685
+ if (args.includeScaffold) {
686
+ const code = generatedPageCode(best.pattern.id, args.intent, locale);
687
+ const componentSource = `${code.script}\n\n${code.template}\n\n${code.style}`;
688
+ result.scaffold = {
689
+ vue: code,
690
+ files: { component: componentSource },
691
+ warnings: [
692
+ locale === 'en-US'
693
+ ? 'Scaffold only: replace sample API state, data, and events with the application implementation.'
694
+ : '仅为脚手架:请将示例 API 状态、数据和事件替换为实际业务实现。',
695
+ ],
696
+ };
697
+ }
698
+ return textResult(result);
699
+ }
700
+ function getDesignRules(args = {}) {
701
+ const locale = resolveLocale(args.mode);
702
+ if (locale === 'zh-CN')
703
+ return textResult(designRules);
704
+ return textResult({
705
+ tokens: {
706
+ colors: ['--m-color-primary', '--m-color-surface', '--m-color-text', '--m-color-border'],
707
+ spacing: '--m-space-*',
708
+ radius: '--m-radius-sm/md/lg',
709
+ typography: '--m-font-size-xs/sm/md/lg',
710
+ motion: '--m-motion-fast/normal',
711
+ },
712
+ actions: {
713
+ primary: { component: 'MButton', props: ['omit severity or use primary'] },
714
+ secondary: { component: 'MButton', props: ['severity="secondary"', 'outlined or text'] },
715
+ destructive: { component: 'MButton', props: ['severity="danger"'], requiresConfirmation: true },
716
+ cancel: { component: 'MButton', props: ['severity="secondary"', 'text'] },
717
+ },
718
+ status: { component: 'MTag', mapping: { active: 'success', pending: 'warn', disabled: 'secondary', error: 'danger' } },
719
+ feedback: {
720
+ default: 'message',
721
+ message: { when: ['single-line action result', 'save/delete/create confirmations'] },
722
+ toast: { when: ['summary + detail', 'async or background notifications'] },
723
+ inlineMessage: { component: 'MMessage', when: ['persistent form/auth errors'] },
724
+ doc: 'docs/feedback-message-vs-toast.md',
725
+ },
726
+ global: [
727
+ 'Prefer library components and --m-* tokens; do not maintain a second color system.',
728
+ 'Default action feedback to message; do not use toast with summary-only text.',
729
+ 'Icon-only buttons must provide aria-label or ariaLabel.',
730
+ 'Form controls must have a visible label or an equivalent accessible name.',
731
+ 'Overlays teleport to body by default; only change appendTo for a clear layout constraint.',
732
+ 'Prefer documented component variants over deep CSS overrides.',
733
+ ],
734
+ });
735
+ }
736
+ function recommendComponent(args) {
737
+ const locale = resolveLocale(args.mode);
738
+ if (!args.query && !args.decision) {
739
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
740
+ const offset = Math.max(args.offset ?? 0, 0);
741
+ const items = componentDecisions.slice(offset, offset + limit).map((decision) => ({
742
+ id: decision.id,
743
+ title: locale === 'en-US' ? decision.titleEn : decision.title,
744
+ question: locale === 'en-US' ? decision.questionEn : decision.question,
745
+ keywords: decision.keywords,
746
+ options: decision.options.map((option) => option.component),
747
+ }));
748
+ return textResult({ kind: 'decisions', ...pagination(componentDecisions.length, offset, limit), items });
749
+ }
750
+ if (args.decision && !args.query) {
751
+ const decision = findDecision(args.decision);
752
+ if (!decision) {
753
+ return textResult({
754
+ error: `Decision not found: ${args.decision}`,
755
+ availableDecisions: componentDecisions.map((item) => item.id),
756
+ });
757
+ }
758
+ return textResult({
759
+ id: decision.id,
760
+ title: locale === 'en-US' ? decision.titleEn : decision.title,
761
+ question: locale === 'en-US' ? decision.questionEn : decision.question,
762
+ keywords: decision.keywords,
763
+ options: decision.options.map((option) => ({
764
+ component: option.component,
765
+ when: locale === 'en-US' ? option.whenEn : option.when,
766
+ avoidWhen: locale === 'en-US' ? option.avoidWhenEn : option.avoidWhen,
767
+ })),
768
+ });
769
+ }
770
+ if (!args.query) {
771
+ return textResult({
772
+ error: 'Provide query for a recommendation, or omit query/decision to list decision guides.',
773
+ });
774
+ }
775
+ const ranked = componentDecisions
776
+ .map((decision) => ({ decision, score: scoreDecision(decision, args.query) }))
777
+ .sort((a, b) => b.score - a.score || a.decision.id.localeCompare(b.decision.id));
778
+ const matched = args.decision ? findDecision(args.decision) : ranked[0]?.decision;
779
+ if (!matched || (!args.decision && (ranked[0]?.score || 0) === 0)) {
780
+ return textResult({ error: `No component decision matched: ${args.query}`, suggestions: componentDecisions.map((item) => item.id) });
781
+ }
782
+ return textResult({
783
+ query: args.query,
784
+ decision: matched.id,
785
+ title: locale === 'en-US' ? matched.titleEn : matched.title,
786
+ question: locale === 'en-US' ? matched.questionEn : matched.question,
787
+ recommendations: matched.options.map((option) => ({
788
+ component: option.component,
789
+ when: locale === 'en-US' ? option.whenEn : option.when,
790
+ avoidWhen: locale === 'en-US' ? option.avoidWhenEn : option.avoidWhen,
791
+ })),
792
+ nextStep: locale === 'en-US'
793
+ ? 'Use get_component and get_example for the selected component before implementing.'
794
+ : '实现前请用 get_component 和 get_example 核对所选组件 API。',
795
+ });
796
+ }
797
+ function getSetup(args) {
798
+ const locale = resolveLocale(args.mode);
799
+ const quickStart = findGuide(catalog, 'quick-start');
800
+ const config = findGuide(catalog, 'config');
801
+ const theme = findGuide(catalog, 'theme');
802
+ const intro = findGuide(catalog, 'introduction');
803
+ const pickMarkdown = (guide) => {
804
+ if (!guide)
805
+ return null;
806
+ const local = pickLocale(guide, locale);
807
+ return {
808
+ id: guide.id,
809
+ title: local?.title || guide.title,
810
+ markdown: local?.markdown || '',
811
+ };
812
+ };
813
+ return textResult({
814
+ library: catalog.library,
815
+ environment: args.environment || 'vue3-vite',
816
+ install: 'pnpm add morya-ui',
817
+ peer: 'vue@^3.3.0',
818
+ styles: "import 'morya-ui/styles.css'",
819
+ guides: {
820
+ introduction: pickMarkdown(intro),
821
+ quickStart: pickMarkdown(quickStart),
822
+ config: pickMarkdown(config),
823
+ theme: pickMarkdown(theme),
824
+ },
825
+ });
826
+ }
827
+ function validateUsage(args) {
828
+ const usages = args.usages && args.usages.length > 0
829
+ ? args.usages
830
+ : [{ component: args.component, code: args.code }];
831
+ const reports = usages.slice(0, 10).map((usage) => {
832
+ const code = usage.code || '';
833
+ const componentTagMatch = code.match(/<(M)([A-Z][A-Za-z0-9]*)\b/);
834
+ const componentImportMatch = code.match(/import\s*\{[^}]*\b(M)([A-Z][A-Za-z0-9]*)\b/);
835
+ const componentName = usage.component ||
836
+ (componentTagMatch ? `${componentTagMatch[1]}${componentTagMatch[2]}` : undefined) ||
837
+ (componentImportMatch ? `${componentImportMatch[1]}${componentImportMatch[2]}` : undefined);
838
+ if (!componentName) {
839
+ return { error: 'Could not determine component. Pass component explicitly.' };
840
+ }
841
+ const component = findComponent(catalog, componentName);
842
+ if (!component)
843
+ return { component: componentName, error: `Component not found: ${componentName}` };
844
+ const knownProps = new Set(component.props.flatMap((prop) => [prop.name, toKebab(prop.name)].filter(Boolean)));
845
+ const knownEvents = new Set(component.events.flatMap((event) => {
846
+ const name = event.name.replace(/^on/, '');
847
+ return [event.name, name, toKebab(name), `on${name[0]?.toUpperCase()}${name.slice(1)}`];
848
+ }));
849
+ const issues = [];
850
+ if (code && !code.includes('morya-ui') && /import\s+/.test(code)) {
851
+ if (!/from\s+['"]morya-ui['"]/.test(code)) {
852
+ issues.push({
853
+ type: 'import',
854
+ message: `Import should come from 'morya-ui' (expected ${component.exportName}).`,
855
+ });
856
+ }
857
+ }
858
+ const attrRe = /<M[A-Z][A-Za-z0-9]*\b([^>]*)>/g;
859
+ let tagMatch = attrRe.exec(code);
860
+ while (tagMatch !== null) {
861
+ const attrs = tagMatch[1] || '';
862
+ const attrNames = [
863
+ ...attrs.matchAll(/(?:^|\s)(?:v-bind:|:)([A-Za-z_][\w-]*)/g),
864
+ ...attrs.matchAll(/(?:^|\s)([A-Z_][\w-]*)\s*=/gi),
865
+ ...attrs.matchAll(/(?:^|\s)(v-model(?:\.[\w-]+)?)/g),
866
+ ].map((match) => match[1]);
867
+ for (const attr of attrNames) {
868
+ if (!attr || attr.startsWith('v-') || attr === 'class' || attr === 'style' || attr === 'key') {
869
+ continue;
870
+ }
871
+ if (attr.startsWith('on') || attr.startsWith('@'))
872
+ continue;
873
+ if (!knownProps.has(attr) && !knownProps.has(toKebab(attr))) {
874
+ // event listeners written as @click already skipped; allow aria-* and data-*
875
+ if (attr.startsWith('aria-') || attr.startsWith('data-'))
876
+ continue;
877
+ if (knownProps.size > 0) {
878
+ issues.push({
879
+ type: 'unknown-prop',
880
+ message: `Unknown prop '${attr}' on ${component.exportName}.`,
881
+ });
882
+ }
883
+ }
884
+ }
885
+ const eventNames = [...attrs.matchAll(/(?:^|\s)@([A-Z_][\w-]*)/gi)].map((match) => match[1]);
886
+ for (const eventName of eventNames) {
887
+ if (knownEvents.size === 0)
888
+ continue;
889
+ if (!knownEvents.has(eventName) &&
890
+ !knownEvents.has(toKebab(eventName)) &&
891
+ eventName !== 'click') {
892
+ // soft warning only when events are documented and clearly unknown
893
+ if (![...knownEvents].some((item) => normalizeName(item) === normalizeName(eventName))) {
894
+ issues.push({
895
+ type: 'unknown-event',
896
+ message: `Event '@${eventName}' is not listed in ${component.exportName} docs.`,
897
+ });
898
+ }
899
+ }
900
+ }
901
+ tagMatch = attrRe.exec(code);
902
+ }
903
+ if (component.id === 'Button' && code) {
904
+ inspectButtonIconOnlyUsage(code, issues);
905
+ }
906
+ return {
907
+ component: component.id,
908
+ exportName: component.exportName,
909
+ ok: issues.length === 0,
910
+ issues,
911
+ knownProps: component.props.map((item) => item.name),
912
+ knownEvents: component.events.map((item) => item.name),
913
+ };
914
+ });
915
+ return textResult(reports.length === 1 ? reports[0] : { reports });
916
+ }
917
+ function version() {
918
+ const patternReferences = pagePatterns.flatMap((pattern) => pattern.components
919
+ .filter((item) => !findComponent(catalog, item.component))
920
+ .map((item) => ({ pattern: pattern.id, component: item.component })));
921
+ return textResult({
922
+ mcp: catalog.mcp,
923
+ library: catalog.library,
924
+ generatedAt: catalog.generatedAt,
925
+ counts: {
926
+ components: catalog.components.length,
927
+ guides: catalog.guides.length,
928
+ examples: catalog.components.reduce((sum, item) => sum + item.examples.length, 0),
929
+ patterns: pagePatterns.length,
930
+ decisions: componentDecisions.length,
931
+ resources: countCatalogResources(catalog),
932
+ resourceTemplates: countCatalogResourceTemplates(),
933
+ },
934
+ health: {
935
+ ok: patternReferences.length === 0,
936
+ patternReferences,
937
+ catalogGeneratedAt: catalog.generatedAt,
938
+ message: patternReferences.length === 0
939
+ ? 'Catalog and pattern references are consistent.'
940
+ : 'Some patterns reference components missing from the catalog.',
941
+ },
942
+ tools: [
943
+ 'list',
944
+ 'search',
945
+ 'get_component',
946
+ 'get_example',
947
+ 'get_guide',
948
+ 'get_setup',
949
+ 'validate_usage',
950
+ 'list_patterns',
951
+ 'get_pattern',
952
+ 'recommend_page',
953
+ 'get_design_rules',
954
+ 'recommend_component',
955
+ 'version',
956
+ ],
957
+ });
958
+ }
959
+ return {
960
+ catalog,
961
+ list,
962
+ search,
963
+ getComponent,
964
+ getExample,
965
+ getGuide,
966
+ getSetup,
967
+ validateUsage,
968
+ listPatterns,
969
+ getPattern,
970
+ recommendPage,
971
+ getDesignRules,
972
+ recommendComponent,
973
+ version,
974
+ };
975
+ }