@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.
@@ -0,0 +1,383 @@
1
+ export const pagePatterns = [
2
+ {
3
+ id: 'admin-list',
4
+ title: '管理后台列表页',
5
+ titleEn: 'Admin list page',
6
+ description: '用于资源管理、设备管理、用户管理等带筛选和行操作的数据列表。',
7
+ descriptionEn: 'A resource-management list with filters, actions, row operations, and pagination.',
8
+ goldenPage: 'docs/golden-pages/list-page.vue',
9
+ keywords: [
10
+ '列表',
11
+ '表格',
12
+ '数据表',
13
+ '数据列表',
14
+ '管理',
15
+ '后台',
16
+ '筛选',
17
+ '搜索',
18
+ '分页',
19
+ '新增',
20
+ '编辑',
21
+ '删除',
22
+ 'list',
23
+ 'table',
24
+ 'filter',
25
+ 'pagination',
26
+ 'crud',
27
+ ],
28
+ components: [
29
+ { component: 'ConfigProvider', role: '应用根包裹', reason: '统一 locale、主题、密度与浮层挂载' },
30
+ { component: 'Layout', role: '后台应用骨架', reason: 'has-sider 侧栏 + 主内容区' },
31
+ { component: 'LayoutSider', role: '侧栏导航', required: false },
32
+ { component: 'LayoutHeader', role: '顶栏面包屑' },
33
+ { component: 'LayoutContent', role: '页面主体内容区' },
34
+ { component: 'Breadcrumb', role: '页面层级导航' },
35
+ { component: 'Space', role: '筛选区与行操作间距布局' },
36
+ { component: 'Input', role: '关键词筛选' },
37
+ { component: 'Select', role: '枚举或状态筛选' },
38
+ { component: 'Button', role: '查询、新增和行操作' },
39
+ { component: 'Table', role: '数据展示', reason: 'paginator 启用内置分页' },
40
+ { component: 'Tag', role: '业务状态展示' },
41
+ { component: 'Dialog', role: '编辑或删除确认', required: false },
42
+ ],
43
+ structure: [
44
+ 'MConfigProvider',
45
+ '└── MLayout (has-sider)',
46
+ ' ├── MLayoutSider → MMenu (optional)',
47
+ ' └── MLayout',
48
+ ' ├── MLayoutHeader → MBreadcrumb',
49
+ ' └── MLayoutContent',
50
+ ' ├── Filter section (MSpace + MInput + MSelect + MButton)',
51
+ ' ├── PageHeader (title + primary action)',
52
+ ' └── MTable (paginator, #cell-* slots, #empty)',
53
+ ],
54
+ layout: {
55
+ page: 'MLayout has-sider 作为后台骨架;内容区 padding 使用 --m-space-6',
56
+ header: 'MLayoutHeader 放面包屑;标题与主操作在内容区 toolbar 两端对齐',
57
+ filters: '筛选区使用浅色背景 + border-radius;桌面端 MSpace wrap,窄屏堆叠',
58
+ data: 'MTable 启用 paginator;行操作用 MButton size="small";空态用 #empty 插槽',
59
+ },
60
+ styleRules: [
61
+ '新增、保存等主操作使用 primary MButton',
62
+ '查询等次要操作使用 secondary 或 outlined MButton',
63
+ '删除使用 danger MButton,并配合 MDialog 或 MConfirmDialog',
64
+ '业务状态使用 MTag,不使用 Button severity 表达状态',
65
+ '间距、颜色、圆角优先使用 --m-* 设计令牌,不直接硬编码色板',
66
+ ],
67
+ interactionRules: [
68
+ '筛选提交后重置分页到第一页',
69
+ '加载中禁用重复提交并显示 loading 状态',
70
+ '空数据使用 MTable #empty 插槽或 DataView 空态,不渲染空白表格',
71
+ '行级危险操作必须有确认反馈',
72
+ 'CRUD 成功/删除等单行回执使用 message API,不要默认 Toast',
73
+ ],
74
+ avoid: [
75
+ '不要用 MDialog 承载完整列表页',
76
+ '不要用 MButton 代替业务状态标签',
77
+ '不要为每个筛选字段手写一套不一致的 label 和间距',
78
+ '不要用 toast.add({ summary }) 代替 message.success 表达单行操作结果',
79
+ ],
80
+ },
81
+ {
82
+ id: 'form-page',
83
+ title: '新增 / 编辑表单页',
84
+ titleEn: 'Create or edit form page',
85
+ description: '用于创建或编辑业务对象,包含分组字段、校验和提交状态。',
86
+ descriptionEn: 'A create or edit page with grouped fields, validation, and submit states.',
87
+ goldenPage: 'docs/golden-pages/form-page.vue',
88
+ keywords: ['表单', '新增', '编辑', '创建', '配置', '设置', 'form', 'create', 'edit', 'settings'],
89
+ components: [
90
+ { component: 'ConfigProvider', role: '应用根包裹' },
91
+ { component: 'Layout', role: '页面骨架' },
92
+ { component: 'LayoutHeader', role: '顶栏面包屑' },
93
+ { component: 'LayoutContent', role: '表单主体区', reason: 'max-width 约 42rem' },
94
+ { component: 'Breadcrumb', role: '页面层级导航' },
95
+ { component: 'Form', role: '声明式校验和布局' },
96
+ { component: 'FormItem', role: '字段标签、帮助文本和校验状态', required: false, reason: '由 MForm 同目录导出,作为表单子结构使用' },
97
+ { component: 'Input', role: '文本字段' },
98
+ { component: 'Select', role: '枚举字段' },
99
+ { component: 'Textarea', role: '长文本字段', required: false },
100
+ { component: 'Button', role: '提交和取消' },
101
+ ],
102
+ structure: [
103
+ 'MConfigProvider',
104
+ '└── MLayout',
105
+ ' ├── MLayoutHeader → MBreadcrumb',
106
+ ' └── MLayoutContent',
107
+ ' ├── Page intro (title + description)',
108
+ ' └── MForm',
109
+ ' ├── MFormItem fields',
110
+ ' └── Actions (submit + cancel)',
111
+ ],
112
+ layout: {
113
+ shell: 'MLayout + MLayoutHeader + MLayoutContent;表单 max-width 约 42rem',
114
+ page: '表单主体使用 max-width,避免字段横向过宽',
115
+ form: '默认 label-position=top;字段按业务逻辑分组',
116
+ actions: '提交按钮在前,取消使用 text 或 secondary,操作区与表单分隔',
117
+ responsive: '多列字段在窄屏降为单列',
118
+ },
119
+ styleRules: [
120
+ '统一使用 MForm + MFormItem 管理校验,不在页面中散落错误文案',
121
+ '字段控件通常使用 fluid,宽度由表单布局控制',
122
+ '提交使用 primary MButton,保存中传入 loading',
123
+ '取消使用 secondary text MButton,避免与提交按钮争夺视觉层级',
124
+ '使用 MConfigProvider 或 --m-* Token 保持尺寸和间距一致',
125
+ ],
126
+ interactionRules: [
127
+ '提交前调用 FormInstance.validate()',
128
+ '校验失败时保留用户输入并聚焦第一个错误字段',
129
+ '异步保存期间防止重复提交',
130
+ '离开有未保存修改的页面时给出确认提示',
131
+ ],
132
+ avoid: [
133
+ '不要绕过 MForm 直接手写不一致的字段校验状态',
134
+ '不要把完整编辑流程塞进过窄的 Dialog,除非字段很少',
135
+ '不要使用 placeholder 代替 label',
136
+ ],
137
+ },
138
+ {
139
+ id: 'dashboard',
140
+ title: '仪表盘 / 监控工作台',
141
+ titleEn: 'Dashboard / monitoring workspace',
142
+ description: '用于展示关键指标、趋势、告警和需要优先处理的业务信息。',
143
+ descriptionEn: 'A workspace for KPIs, trends, alerts, and prioritized operational information.',
144
+ keywords: ['仪表盘', '工作台', '监控', '指标', '趋势', '告警', 'dashboard', 'monitoring', 'kpi', 'analytics'],
145
+ components: [
146
+ { component: 'ConfigProvider', role: '应用根包裹' },
147
+ { component: 'Layout', role: '页面骨架' },
148
+ { component: 'LayoutHeader', role: '顶栏与面包屑' },
149
+ { component: 'LayoutContent', role: '仪表盘内容' },
150
+ { component: 'Breadcrumb', role: '页面层级导航' },
151
+ { component: 'Grid', role: 'KPI 与内容区栅格' },
152
+ { component: 'GridItem', role: '栅格子项' },
153
+ { component: 'Card', role: '指标和内容分区' },
154
+ { component: 'Icon', role: 'KPI 图标', required: false },
155
+ { component: 'Tag', role: '状态和告警级别' },
156
+ { component: 'Table', role: '明细数据', required: false },
157
+ { component: 'Button', role: '刷新、查看详情和快捷操作', required: false },
158
+ { component: 'Skeleton', role: '初始加载占位', required: false },
159
+ ],
160
+ goldenPage: 'docs/golden-pages/dashboard-page.vue',
161
+ structure: [
162
+ 'ConfigProvider',
163
+ '└── Layout',
164
+ ' ├── LayoutHeader + Breadcrumb',
165
+ ' └── LayoutContent',
166
+ ' ├── Page title',
167
+ ' ├── KPI Grid (Card + Icon)',
168
+ ' ├── Trend / chart Card (placeholder or external chart)',
169
+ ' └── Recent activity Table (optional)',
170
+ ],
171
+ layout: {
172
+ shell: '使用 MLayout + MLayoutHeader + MLayoutContent',
173
+ page: '以 MGrid 组织指标卡和内容卡,避免用绝对定位拼大屏',
174
+ kpi: '指标卡保持等宽,主指标、变化量和时间范围层级清晰',
175
+ content: '图表和明细按业务优先级排列,重要告警靠前',
176
+ responsive: '多列网格在窄屏降为单列或两列',
177
+ },
178
+ styleRules: ['指标使用 text hierarchy,不用大面积高饱和背景', '状态和告警使用 MTag 等语义组件', '图表容器使用 Card 和 --m-* Token 保持表面一致', '实时刷新操作使用 secondary 或 text MButton'],
179
+ interactionRules: ['首次加载使用 Skeleton', '无数据和接口错误必须有明确状态', '刷新中禁用重复请求并保留上次数据', '告警需要可追踪到详情或处理入口'],
180
+ avoid: ['不要把所有信息都做成 KPI 卡片', '不要使用颜色作为唯一告警表达', '不要用固定像素绝对定位实现响应式布局'],
181
+ },
182
+ {
183
+ id: 'settings-page',
184
+ title: '设置 / 配置页',
185
+ titleEn: 'Settings / configuration page',
186
+ description: '用于偏好设置、系统配置、权限配置和分组表单。',
187
+ descriptionEn: 'A grouped configuration page for preferences, system settings, and permissions.',
188
+ keywords: ['设置', '配置', '偏好', '权限', '系统设置', 'settings', 'configuration', 'preferences', 'permissions'],
189
+ components: [
190
+ { component: 'Tabs', role: '多个设置域之间切换' },
191
+ { component: 'Card', role: '设置分组表面' },
192
+ { component: 'Form', role: '配置表单' },
193
+ { component: 'FormItem', role: '字段标签和校验' },
194
+ { component: 'Input', role: '文本配置' },
195
+ { component: 'Select', role: '枚举配置' },
196
+ { component: 'Switch', role: '启用 / 禁用配置', required: false },
197
+ { component: 'Button', role: '保存和恢复默认值' },
198
+ ],
199
+ structure: ['Page', '├── PageHeader', '├── Tabs (optional)', '│ └── Settings section', '│ └── Card + Form', '└── Save actions'],
200
+ layout: { page: '设置内容使用稳定的 max-width,避免控件铺满视口', navigation: '设置域较多时使用 Tabs 或 Sidebar,较少时使用连续分组', form: 'label-position=top,相关字段使用 Card 或 Fieldset 分组', actions: '保存操作固定在表单末尾或明确的 sticky action bar' },
201
+ styleRules: ['开关用于二元启用状态,不用 Select 模拟开关', '保存使用 primary,恢复默认值使用 secondary 或 text', '危险配置需要明确说明影响范围并使用 danger 语义', '使用 MConfigProvider 统一 size、density 和 locale'],
202
+ interactionRules: ['显示未保存修改状态', '保存成功使用 message.success 单行反馈;仅有 summary+detail 时用 toast', '保存失败保留输入并显示字段或页面级错误', '切换 Tab 不应意外丢失未保存输入'],
203
+ avoid: ['不要把所有设置塞进一个超长表单', '不要用 placeholder 代替配置项 label', '不要隐藏影响范围较大的配置说明'],
204
+ },
205
+ {
206
+ id: 'empty-state',
207
+ title: '空状态 / 无结果页',
208
+ titleEn: 'Empty state / no results page',
209
+ description: '用于首次使用、搜索无结果、资源已清空或暂时没有内容的场景。',
210
+ descriptionEn: 'For first use, no search results, empty resources, or temporarily unavailable content.',
211
+ keywords: ['空状态', '无数据', '无结果', '首次使用', 'empty', 'no results', 'no data', 'zero state'],
212
+ components: [
213
+ { component: 'DataView', role: '列表或网格空态承载' },
214
+ { component: 'Button', role: '创建、重置筛选或返回', required: false },
215
+ { component: 'Card', role: '在列表或详情容器中承载空态', required: false },
216
+ ],
217
+ structure: ['Container or Card', '└── DataView / Table #empty', ' ├── Explanation', ' └── Primary recovery action (optional)'],
218
+ layout: { page: '空态在当前内容区域内居中,不一定占满整个视口', content: '说明原因、下一步动作和可选的辅助信息', responsive: '保持按钮可触达,窄屏垂直排列' },
219
+ styleRules: ['空态文案使用中性、可行动的语气', '主恢复动作使用 primary MButton,次动作使用 text', '不要用错误色表达正常的无数据状态'],
220
+ interactionRules: ['搜索无结果提供清除筛选或修改条件入口', '首次使用提供创建或导入入口', '保留页面标题和上下文导航'],
221
+ avoid: ['不要只显示“暂无数据”而没有下一步', '不要把无数据和接口错误混为一谈'],
222
+ },
223
+ {
224
+ id: 'auth-page',
225
+ title: '登录 / 认证页',
226
+ titleEn: 'Login / authentication page',
227
+ description: '用于登录、注册、找回密码和二次认证流程。',
228
+ descriptionEn: 'For login, registration, password recovery, and second-factor authentication flows.',
229
+ keywords: ['登录', '注册', '认证', '密码', '验证码', 'login', 'register', 'authentication', 'password', 'otp'],
230
+ components: [
231
+ { component: 'Card', role: '认证表面' },
232
+ { component: 'Form', role: '认证字段校验' },
233
+ { component: 'FormItem', role: '字段标签和错误信息', required: false, reason: '由 MForm 同目录导出,作为表单子结构使用' },
234
+ { component: 'Input', role: '账号或密码字段' },
235
+ { component: 'InputOtp', role: '验证码字段', required: false },
236
+ { component: 'Button', role: '提交认证' },
237
+ { component: 'Message', role: '页面级反馈', required: false },
238
+ ],
239
+ structure: ['AuthLayout', '└── Card', ' └── Form', ' ├── Identity fields', ' ├── Recovery / alternative links', ' └── Submit action'],
240
+ layout: { page: '认证表单使用窄 max-width 并保持视觉聚焦', form: '字段垂直排列,提交按钮 fluid', 'feedback': '错误信息靠近字段,认证失败提供页面级总结' },
241
+ styleRules: ['密码字段使用 InputPassword 或 documented password API', '提交按钮使用 primary + fluid', '不要用高对比装饰削弱错误信息和焦点状态', '品牌视觉可定制,但控件状态仍使用 --m-* Token'],
242
+ interactionRules: ['提交中显示 loading 并防止重复提交', '错误不清空用户已填写的非敏感字段', '密码输入和验证码输入提供可访问名称', '支持键盘提交和清晰焦点顺序'],
243
+ avoid: ['不要用 placeholder 代替字段 label', '不要把所有认证错误只放在 Toast 中', '不要在错误时回显密码'],
244
+ },
245
+ {
246
+ id: 'wizard-form',
247
+ title: '分步表单 / 向导',
248
+ titleEn: 'Multi-step form / wizard',
249
+ description: '用于字段较多、需要按顺序完成或有阶段性校验的流程。',
250
+ descriptionEn: 'For long workflows that need ordered steps and staged validation.',
251
+ keywords: ['分步', '向导', '步骤', '流程', 'wizard', 'stepper', 'multi-step', 'workflow'],
252
+ components: [
253
+ { component: 'Stepper', role: '步骤导航' },
254
+ { component: 'Form', role: '当前步骤字段校验' },
255
+ { component: 'FormItem', role: '字段标签和校验' },
256
+ { component: 'Input', role: '文本字段', required: false },
257
+ { component: 'Select', role: '选择字段', required: false },
258
+ { component: 'Button', role: '上一步、下一步和完成' },
259
+ { component: 'Dialog', role: '离开或取消确认', required: false },
260
+ ],
261
+ structure: ['Page', '├── Stepper', '├── Current step Card', '│ └── Form', '└── Navigation actions'],
262
+ layout: { page: '步骤导航和表单主体保持同一内容边界', stepper: '步骤少时横向展示,步骤多或窄屏时允许纵向或滚动', actions: '上一步为 secondary,下一步和完成为 primary' },
263
+ styleRules: ['每一步只承载一个清晰目标', '步骤状态使用 Stepper,不用 Button 颜色模拟', '长流程使用 Card 分组但避免多层嵌套', '危险退出使用 ConfirmDialog 或 Dialog'],
264
+ interactionRules: ['进入下一步前只校验当前步骤', '返回上一步保留输入', '完成前展示摘要或确认', '刷新和离开时处理未完成状态'],
265
+ avoid: ['不要把所有字段一次性隐藏在一个超长页面', '不要允许跳过有前置依赖的步骤', '不要让完成按钮在每一步都使用相同文案'],
266
+ },
267
+ {
268
+ id: 'detail-page',
269
+ title: '详情页',
270
+ titleEn: 'Detail page',
271
+ description: '用于查看一个资源的摘要、属性、状态和相关操作。',
272
+ descriptionEn: 'A resource detail view with summary, properties, status, and contextual actions.',
273
+ keywords: ['详情', '明细', '查看', '概览', '属性', '详情页', 'detail', 'overview', 'profile'],
274
+ components: [
275
+ { component: 'Breadcrumb', role: '页面层级导航' },
276
+ { component: 'Card', role: '内容分组表面' },
277
+ { component: 'Tag', role: '状态展示' },
278
+ { component: 'Divider', role: '内容分组分隔', required: false },
279
+ { component: 'Button', role: '编辑、返回和危险操作' },
280
+ { component: 'Dialog', role: '危险操作确认', required: false },
281
+ ],
282
+ structure: [
283
+ 'Page',
284
+ '├── Breadcrumb',
285
+ '├── PageHeader',
286
+ '│ ├── Title + Status Tag',
287
+ '│ └── Actions',
288
+ '├── Summary Card',
289
+ '├── Properties Card',
290
+ '└── Related content',
291
+ ],
292
+ layout: {
293
+ page: '使用清晰的标题区和多个语义分组,避免一张超长 Card',
294
+ header: '资源名称、状态和上下文操作集中在顶部',
295
+ content: '属性区可用 CSS grid,多列信息在窄屏降为单列',
296
+ actions: '编辑为主操作,返回为次操作,删除放在低强调区域',
297
+ },
298
+ styleRules: [
299
+ '状态使用 MTag 的语义色',
300
+ '内容分组使用 MCard,必要时用 MDivider 分隔',
301
+ '编辑使用 primary 或 outlined MButton,删除使用 danger',
302
+ '不使用大面积自定义背景色覆盖组件库 surface Token',
303
+ ],
304
+ interactionRules: [
305
+ '详情加载中使用 Skeleton 或 ProgressSpinner',
306
+ '资源不存在时提供明确的 Table #empty 或 DataView 空态和返回入口',
307
+ '危险操作必须确认并在成功后刷新或离开当前详情页',
308
+ ],
309
+ avoid: [
310
+ '不要把所有属性拼成一段无层次文本',
311
+ '不要使用颜色作为唯一的状态表达方式',
312
+ '不要让详情页的操作按钮分散在多个无关区域',
313
+ ],
314
+ },
315
+ ];
316
+ export const designRules = {
317
+ tokens: {
318
+ colors: ['--m-color-primary', '--m-color-surface', '--m-color-text', '--m-color-border'],
319
+ spacing: '--m-space-*',
320
+ radius: '--m-radius-sm/md/lg',
321
+ typography: '--m-font-size-xs/sm/md/lg',
322
+ motion: '--m-motion-fast/normal',
323
+ },
324
+ actions: {
325
+ primary: { component: 'MButton', props: ['severity omitted or primary'] },
326
+ secondary: { component: 'MButton', props: ['severity="secondary"', 'outlined or text'] },
327
+ destructive: { component: 'MButton', props: ['severity="danger"'], requiresConfirmation: true },
328
+ cancel: { component: 'MButton', props: ['severity="secondary"', 'text'] },
329
+ },
330
+ status: {
331
+ component: 'MTag',
332
+ mapping: { active: 'success', pending: 'warn', disabled: 'secondary', error: 'danger' },
333
+ },
334
+ feedback: {
335
+ default: 'message',
336
+ message: {
337
+ api: 'message.success | info | warn | error',
338
+ when: ['单行操作回执(已保存/已删除/已创建)', '轻量警告或错误', '复制成功等一句话反馈'],
339
+ avoid: ['不要写 summary/detail 结构'],
340
+ },
341
+ toast: {
342
+ api: 'toast.success | info | warn | error({ summary, detail? })',
343
+ when: ['同时需要标题与补充说明', '后台任务/批量结果含统计', '异步通知感、角落堆叠'],
344
+ avoid: ['仅有单行文案时不要使用 Toast'],
345
+ },
346
+ inlineMessage: {
347
+ component: 'MMessage',
348
+ when: ['登录/表单区常驻错误', '页面级错误需与表单同区展示'],
349
+ },
350
+ doc: 'docs/feedback-message-vs-toast.md',
351
+ },
352
+ global: [
353
+ '优先使用组件库组件和 --m-* Token,不重复维护第二套色板',
354
+ '操作反馈默认 message;无 detail 禁止用 toast.add({ summary only })',
355
+ '图标按钮必须提供 aria-label 或 ariaLabel',
356
+ '表单字段必须有可见 label 或等价的可访问名称',
357
+ '浮层默认 Teleport 到 body;只有有明确布局约束时才改 appendTo',
358
+ '优先使用组件的 documented variant,不通过深层 CSS 覆盖内部样式',
359
+ ],
360
+ };
361
+ export function findPattern(input) {
362
+ const key = input.trim().toLowerCase().replace(/[-_\s]/g, '');
363
+ return pagePatterns.find((pattern) => pattern.id.replace(/[-_\s]/g, '') === key);
364
+ }
365
+ export function scorePattern(pattern, query) {
366
+ const normalized = query.toLowerCase().trim();
367
+ if (!normalized)
368
+ return 0;
369
+ const parts = normalized.split(/[\s,,、/]+/).filter(Boolean);
370
+ let score = 0;
371
+ for (const part of parts) {
372
+ for (const keyword of pattern.keywords) {
373
+ const normalizedKeyword = keyword.toLowerCase();
374
+ if (normalizedKeyword === part)
375
+ score += 30;
376
+ else if (normalizedKeyword.includes(part) || part.includes(normalizedKeyword))
377
+ score += 12;
378
+ }
379
+ }
380
+ if (pattern.id === normalized.replace(/[-_\s]/g, ''))
381
+ score += 100;
382
+ return score;
383
+ }
@@ -0,0 +1,5 @@
1
+ import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { Catalog } from './catalog.js';
3
+ export declare function registerCatalogResources(server: McpServer, catalog: Catalog): void;
4
+ export declare function countCatalogResources(catalog: Catalog): number;
5
+ export declare function countCatalogResourceTemplates(): number;
@@ -0,0 +1,205 @@
1
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { designRules } from './patterns.js';
3
+ const DOC_LOCALES = ['zh-CN', 'en-US'];
4
+ const RESOURCE_TEMPLATE_COUNT = 3;
5
+ function filterCompletion(candidates, value) {
6
+ const query = (value ?? '').trim().toLowerCase();
7
+ if (!query)
8
+ return candidates;
9
+ return candidates.filter((item) => item.toLowerCase().startsWith(query));
10
+ }
11
+ function findComponentById(catalog, componentId) {
12
+ return catalog.components.find((item) => item.id === componentId);
13
+ }
14
+ function findGuideById(catalog, guideId) {
15
+ return catalog.guides.find((item) => item.id === guideId);
16
+ }
17
+ function componentApiPayload(component) {
18
+ return {
19
+ id: component.id,
20
+ exportName: component.exportName,
21
+ category: component.category,
22
+ import: component.import,
23
+ props: component.props,
24
+ events: component.events,
25
+ slots: component.slots,
26
+ methods: component.methods,
27
+ };
28
+ }
29
+ function registerStaticResources(server, catalog) {
30
+ server.registerResource('catalog-index', 'm://catalog/index.json', {
31
+ title: 'Morya UI catalog index',
32
+ description: 'Index of all components and guides in the MCP catalog.',
33
+ mimeType: 'application/json',
34
+ }, async (uri) => ({
35
+ contents: [
36
+ {
37
+ uri: uri.href,
38
+ mimeType: 'application/json',
39
+ text: JSON.stringify({
40
+ library: catalog.library,
41
+ mcp: catalog.mcp,
42
+ generatedAt: catalog.generatedAt,
43
+ components: catalog.components.map((item) => ({
44
+ id: item.id,
45
+ exportName: item.exportName,
46
+ category: item.category,
47
+ docsZh: `m://components/${item.id}/docs/zh-CN`,
48
+ docsEn: `m://components/${item.id}/docs/en-US`,
49
+ api: `m://components/${item.id}/api.json`,
50
+ })),
51
+ guides: catalog.guides.map((item) => ({
52
+ id: item.id,
53
+ title: item.title,
54
+ docsZh: `m://guides/${item.id}/docs/zh-CN`,
55
+ docsEn: `m://guides/${item.id}/docs/en-US`,
56
+ })),
57
+ }, null, 2),
58
+ },
59
+ ],
60
+ }));
61
+ server.registerResource('design-rules', 'm://design-rules.json', {
62
+ title: 'Morya UI design rules',
63
+ description: 'Design tokens, semantic actions, accessibility, and composition rules.',
64
+ mimeType: 'application/json',
65
+ }, async (uri) => ({
66
+ contents: [
67
+ {
68
+ uri: uri.href,
69
+ mimeType: 'application/json',
70
+ text: JSON.stringify(designRules, null, 2),
71
+ },
72
+ ],
73
+ }));
74
+ }
75
+ function registerComponentDocTemplate(server, catalog) {
76
+ const componentIds = catalog.components.map((item) => item.id);
77
+ server.registerResource('component-docs', new ResourceTemplate('m://components/{component}/docs/{locale}', {
78
+ list: async () => ({
79
+ resources: catalog.components.flatMap((component) => DOC_LOCALES.filter((locale) => component.locales[locale]?.markdown).map((locale) => {
80
+ const doc = component.locales[locale];
81
+ return {
82
+ uri: `m://components/${component.id}/docs/${locale}`,
83
+ name: `${component.id}-docs-${locale}`,
84
+ title: `${component.exportName} documentation (${locale})`,
85
+ description: doc.description || component.description,
86
+ mimeType: 'text/markdown',
87
+ };
88
+ })),
89
+ }),
90
+ complete: {
91
+ component: async (value) => filterCompletion(componentIds, value),
92
+ locale: async (value) => filterCompletion([...DOC_LOCALES], value),
93
+ },
94
+ }), {
95
+ title: 'Component documentation',
96
+ description: 'Markdown documentation for a morya-ui component.',
97
+ mimeType: 'text/markdown',
98
+ }, async (uri, variables) => {
99
+ const component = findComponentById(catalog, String(variables.component));
100
+ const locale = String(variables.locale);
101
+ const doc = component?.locales[locale];
102
+ if (!component || !doc?.markdown) {
103
+ throw new Error(`Component documentation not found: ${uri.href}`);
104
+ }
105
+ return {
106
+ contents: [{ uri: uri.href, mimeType: 'text/markdown', text: doc.markdown }],
107
+ };
108
+ });
109
+ }
110
+ function registerComponentApiTemplate(server, catalog) {
111
+ const componentIds = catalog.components.map((item) => item.id);
112
+ server.registerResource('component-api', new ResourceTemplate('m://components/{component}/api.json', {
113
+ list: async () => ({
114
+ resources: catalog.components.map((component) => ({
115
+ uri: `m://components/${component.id}/api.json`,
116
+ name: `${component.id}-api`,
117
+ title: `${component.exportName} API`,
118
+ description: `Props, events, and slots for ${component.exportName}.`,
119
+ mimeType: 'application/json',
120
+ })),
121
+ }),
122
+ complete: {
123
+ component: async (value) => filterCompletion(componentIds, value),
124
+ },
125
+ }), {
126
+ title: 'Component API',
127
+ description: 'Props, events, slots, and methods for a morya-ui component.',
128
+ mimeType: 'application/json',
129
+ }, async (uri, variables) => {
130
+ const component = findComponentById(catalog, String(variables.component));
131
+ if (!component) {
132
+ throw new Error(`Component API not found: ${uri.href}`);
133
+ }
134
+ return {
135
+ contents: [
136
+ {
137
+ uri: uri.href,
138
+ mimeType: 'application/json',
139
+ text: JSON.stringify(componentApiPayload(component), null, 2),
140
+ },
141
+ ],
142
+ };
143
+ });
144
+ }
145
+ function registerGuideDocTemplate(server, catalog) {
146
+ const guideIds = catalog.guides.map((item) => item.id);
147
+ server.registerResource('guide-docs', new ResourceTemplate('m://guides/{guide}/docs/{locale}', {
148
+ list: async () => ({
149
+ resources: catalog.guides.flatMap((guide) => DOC_LOCALES.filter((locale) => guide.locales[locale]?.markdown).map((locale) => {
150
+ const doc = guide.locales[locale];
151
+ return {
152
+ uri: `m://guides/${guide.id}/docs/${locale}`,
153
+ name: `${guide.id}-docs-${locale}`,
154
+ title: `${doc.title} (${locale})`,
155
+ description: doc.description,
156
+ mimeType: 'text/markdown',
157
+ };
158
+ })),
159
+ }),
160
+ complete: {
161
+ guide: async (value) => filterCompletion(guideIds, value),
162
+ locale: async (value) => filterCompletion([...DOC_LOCALES], value),
163
+ },
164
+ }), {
165
+ title: 'Guide documentation',
166
+ description: 'Markdown guide docs for morya-ui.',
167
+ mimeType: 'text/markdown',
168
+ }, async (uri, variables) => {
169
+ const guide = findGuideById(catalog, String(variables.guide));
170
+ const locale = String(variables.locale);
171
+ const doc = guide?.locales[locale];
172
+ if (!guide || !doc?.markdown) {
173
+ throw new Error(`Guide documentation not found: ${uri.href}`);
174
+ }
175
+ return {
176
+ contents: [{ uri: uri.href, mimeType: 'text/markdown', text: doc.markdown }],
177
+ };
178
+ });
179
+ }
180
+ export function registerCatalogResources(server, catalog) {
181
+ registerStaticResources(server, catalog);
182
+ registerComponentDocTemplate(server, catalog);
183
+ registerComponentApiTemplate(server, catalog);
184
+ registerGuideDocTemplate(server, catalog);
185
+ }
186
+ export function countCatalogResources(catalog) {
187
+ let count = 2;
188
+ for (const component of catalog.components) {
189
+ for (const locale of DOC_LOCALES) {
190
+ if (component.locales[locale]?.markdown)
191
+ count += 1;
192
+ }
193
+ count += 1;
194
+ }
195
+ for (const guide of catalog.guides) {
196
+ for (const locale of DOC_LOCALES) {
197
+ if (guide.locales[locale]?.markdown)
198
+ count += 1;
199
+ }
200
+ }
201
+ return count;
202
+ }
203
+ export function countCatalogResourceTemplates() {
204
+ return RESOURCE_TEMPLATE_COUNT;
205
+ }