lime-ui-x-mcp 1.0.0

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.
Files changed (3) hide show
  1. package/index.js +289 -0
  2. package/package.json +41 -0
  3. package/src/loader.js +205 -0
package/index.js ADDED
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from 'node:path';
4
+ import { existsSync } from 'node:fs';
5
+ import { cwd, exit } from 'node:process';
6
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import {
9
+ CallToolRequestSchema,
10
+ ListResourcesRequestSchema,
11
+ ReadResourceRequestSchema,
12
+ ListToolsRequestSchema,
13
+ } from '@modelcontextprotocol/sdk/types.js';
14
+
15
+ import {
16
+ scanComponents,
17
+ getReadmeContent,
18
+ searchComponents,
19
+ categorizeComponents,
20
+ setProjectDir,
21
+ getProjectDir,
22
+ } from './src/loader.js';
23
+
24
+ const projectDir = cwd();
25
+ setProjectDir(projectDir);
26
+
27
+ const uniModulesPath = resolve(projectDir, 'uni_modules');
28
+ if (!existsSync(uniModulesPath)) {
29
+ console.error(`[lime-ui-x-mcp] Warning: uni_modules not found at ${uniModulesPath}`);
30
+ console.error(`[lime-ui-x-mcp] Make sure you're running this command inside a uni-app project with lime-ui-x installed.`);
31
+ }
32
+
33
+ const server = new Server(
34
+ {
35
+ name: 'lime-ui-x-mcp',
36
+ version: '1.0.0',
37
+ },
38
+ {
39
+ capabilities: {
40
+ resources: {},
41
+ tools: {},
42
+ },
43
+ }
44
+ );
45
+
46
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
47
+ const categorized = categorizeComponents();
48
+
49
+ const resources = [];
50
+
51
+ resources.push({
52
+ uri: 'lime-ui://components',
53
+ name: 'LimeUI-X 组件总览',
54
+ description: 'LimeUI-X 组件库所有组件按分类的完整列表',
55
+ mimeType: 'application/json',
56
+ });
57
+
58
+ for (const comp of categorized.flatMap(c => c.components)) {
59
+ if (comp.hasReadme) {
60
+ resources.push({
61
+ uri: `lime-ui://components/${comp.pluginName}/readme`,
62
+ name: `${comp.name} 文档`,
63
+ description: `${comp.name} (${comp.componentTag}) 的完整文档`,
64
+ mimeType: 'text/markdown',
65
+ });
66
+ }
67
+
68
+ resources.push({
69
+ uri: `lime-ui://components/${comp.pluginName}/meta`,
70
+ name: `${comp.name} 元数据`,
71
+ description: `${comp.name} 的 front-matter 元数据`,
72
+ mimeType: 'application/json',
73
+ });
74
+ }
75
+
76
+ return { resources };
77
+ });
78
+
79
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
80
+ const uri = request.params.uri;
81
+
82
+ if (uri === 'lime-ui://components') {
83
+ const categorized = categorizeComponents();
84
+ return {
85
+ contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(categorized, null, 2) }],
86
+ };
87
+ }
88
+
89
+ const readmeMatch = uri.match(/^lime-ui:\/\/components\/(lime-[^/]+)\/readme$/);
90
+ if (readmeMatch) {
91
+ const content = getReadmeContent(readmeMatch[1]);
92
+ if (content) return { contents: [{ uri, mimeType: 'text/markdown', text: content }] };
93
+ throw new Error(`Component readme not found: ${readmeMatch[1]}`);
94
+ }
95
+
96
+ const metaMatch = uri.match(/^lime-ui:\/\/components\/(lime-[^/]+)\/meta$/);
97
+ if (metaMatch) {
98
+ const { components } = scanComponents();
99
+ const comp = components.find(c => c.pluginName === metaMatch[1]);
100
+ if (comp) {
101
+ return {
102
+ contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(comp, null, 2) }],
103
+ };
104
+ }
105
+ throw new Error(`Component not found: ${metaMatch[1]}`);
106
+ }
107
+
108
+ throw new Error(`Resource not found: ${uri}`);
109
+ });
110
+
111
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
112
+ tools: [
113
+ {
114
+ name: 'list_components',
115
+ description: '列出 LimeUI-X 组件库所有组件,按分类分组(基础组件、导航组件、表单组件、反馈组件、展示组件、业务组件、原生组件、API 插件、SDK 工具库)',
116
+ inputSchema: {
117
+ type: 'object',
118
+ properties: {
119
+ category: {
120
+ type: 'string',
121
+ description: '按分类筛选:basic(基础), nav(导航), form(表单), feedback(反馈), display(展示), business(业务), native(原生), api(API插件), sdk(工具库)',
122
+ enum: ['basic', 'nav', 'form', 'feedback', 'display', 'business', 'native', 'api', 'sdk'],
123
+ },
124
+ },
125
+ },
126
+ },
127
+ {
128
+ name: 'get_component_doc',
129
+ description: '获取指定组件的完整文档(readme.md 内容)',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {
133
+ pluginName: { type: 'string', description: '插件名称,如 lime-button、lime-dialog、lime-form' },
134
+ },
135
+ required: ['pluginName'],
136
+ },
137
+ },
138
+ {
139
+ name: 'find_components',
140
+ description: '按关键字搜索组件(名称、描述、标签、分类等)',
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ query: { type: 'string', description: '搜索关键字,如 "按钮"、"表单"、"弹窗"' },
145
+ limit: { type: 'number', description: '返回结果数量上限(默认 20)' },
146
+ },
147
+ required: ['query'],
148
+ },
149
+ },
150
+ {
151
+ name: 'get_component_dependencies',
152
+ description: '获取指定组件及其所有依赖的完整依赖链',
153
+ inputSchema: {
154
+ type: 'object',
155
+ properties: {
156
+ pluginName: { type: 'string', description: '插件名称,如 lime-button、lime-dialog' },
157
+ },
158
+ required: ['pluginName'],
159
+ },
160
+ },
161
+ ],
162
+ }));
163
+
164
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
165
+ const { name, arguments: args } = request.params;
166
+
167
+ switch (name) {
168
+ case 'list_components': {
169
+ const categorized = categorizeComponents();
170
+ const category = args?.category;
171
+ const result = category ? categorized.filter(c => c.id === category) : categorized;
172
+
173
+ return {
174
+ content: [{
175
+ type: 'text',
176
+ text: JSON.stringify({
177
+ totalComponents: result.reduce((sum, cat) => sum + cat.components.length, 0),
178
+ categories: result.map(cat => ({
179
+ id: cat.id,
180
+ name: cat.name,
181
+ count: cat.components.length,
182
+ components: cat.components.map(comp => ({
183
+ id: comp.id,
184
+ name: comp.name,
185
+ description: comp.description,
186
+ componentTag: comp.componentTag,
187
+ pluginName: comp.pluginName,
188
+ hasReadme: comp.hasReadme,
189
+ version: comp.version,
190
+ })),
191
+ })),
192
+ }, null, 2),
193
+ }],
194
+ };
195
+ }
196
+
197
+ case 'get_component_doc': {
198
+ const pluginName = args?.pluginName;
199
+ if (!pluginName) throw new Error('pluginName is required');
200
+
201
+ const content = getReadmeContent(pluginName);
202
+ if (!content) throw new Error(`Component not found or no readme available: ${pluginName}`);
203
+
204
+ return { content: [{ type: 'text', text: content }] };
205
+ }
206
+
207
+ case 'find_components': {
208
+ const query = args?.query;
209
+ if (!query) throw new Error('query is required');
210
+
211
+ const results = searchComponents(query);
212
+ const limited = results.slice(0, args?.limit || 20);
213
+
214
+ return {
215
+ content: [{
216
+ type: 'text',
217
+ text: JSON.stringify({
218
+ query,
219
+ total: results.length,
220
+ returned: limited.length,
221
+ components: limited.map(comp => ({
222
+ id: comp.id,
223
+ name: comp.name,
224
+ description: comp.description,
225
+ componentTag: comp.componentTag,
226
+ category: comp.category,
227
+ tags: comp.tags,
228
+ pluginName: comp.pluginName,
229
+ version: comp.version,
230
+ })),
231
+ }, null, 2),
232
+ }],
233
+ };
234
+ }
235
+
236
+ case 'get_component_dependencies': {
237
+ const pluginName = args?.pluginName;
238
+ if (!pluginName) throw new Error('pluginName is required');
239
+
240
+ const { components } = scanComponents();
241
+ const comp = components.find(c => c.pluginName === pluginName);
242
+ if (!comp) throw new Error(`Component not found: ${pluginName}`);
243
+
244
+ function resolveDepChain(depNames, visited = new Set()) {
245
+ const chain = [];
246
+ for (const dep of depNames) {
247
+ if (visited.has(dep)) continue;
248
+ visited.add(dep);
249
+ const depComp = components.find(c => c.pluginName === dep);
250
+ chain.push(depComp
251
+ ? { pluginName: depComp.pluginName, name: depComp.name, description: depComp.description, componentTag: depComp.componentTag, version: depComp.version }
252
+ : { pluginName: dep, name: dep, isExternal: true }
253
+ );
254
+ if (depComp?.dependencies.length) {
255
+ chain.push(...resolveDepChain(depComp.dependencies, visited));
256
+ }
257
+ }
258
+ return chain;
259
+ }
260
+
261
+ return {
262
+ content: [{
263
+ type: 'text',
264
+ text: JSON.stringify({
265
+ component: { pluginName: comp.pluginName, name: comp.name, componentTag: comp.componentTag },
266
+ directDependencies: comp.dependencies,
267
+ dependencyChain: resolveDepChain(comp.dependencies),
268
+ }, null, 2),
269
+ }],
270
+ };
271
+ }
272
+
273
+ default:
274
+ throw new Error(`Unknown tool: ${name}`);
275
+ }
276
+ });
277
+
278
+ async function main() {
279
+ try {
280
+ const transport = new StdioServerTransport();
281
+ await server.connect(transport);
282
+ console.error(`[lime-ui-x-mcp] Server started | project dir: ${getProjectDir()}`);
283
+ } catch (err) {
284
+ console.error('[lime-ui-x-mcp] Failed to start:', err);
285
+ exit(1);
286
+ }
287
+ }
288
+
289
+ main();
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "lime-ui-x-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for LimeUI-X component library - provides component discovery, documentation query, and dependency analysis for AI assistants",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "lime-ui-x-mcp": "./index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "src/",
13
+ "package.json",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "start": "node index.js",
18
+ "dev": "node --watch index.js"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "limeui",
24
+ "uni-app",
25
+ "uni-app-x",
26
+ "ui-components",
27
+ "troe",
28
+ "trae"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.0.0",
36
+ "gray-matter": "^4.0.3"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }
package/src/loader.js ADDED
@@ -0,0 +1,205 @@
1
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
2
+ import { resolve, join } from 'node:path';
3
+ import { cwd } from 'node:process';
4
+ import parseFrontMatter from 'gray-matter';
5
+
6
+ let _projectDir = cwd();
7
+
8
+ const LIME_PREFIX = 'lime-';
9
+
10
+ export function setProjectDir(dir) {
11
+ _projectDir = dir;
12
+ }
13
+
14
+ export function getProjectDir() {
15
+ return _projectDir;
16
+ }
17
+
18
+ function getUniModulesPath() {
19
+ return resolve(_projectDir, 'uni_modules');
20
+ }
21
+
22
+ const COMPONENT_CATEGORIES = [
23
+ { id: 'basic', name: '基础组件', dir: 'basic', order: 1 },
24
+ { id: 'nav', name: '导航组件', dir: 'nav', order: 2 },
25
+ { id: 'form', name: '表单组件', dir: 'form', order: 3 },
26
+ { id: 'feedback', name: '反馈组件', dir: 'feedback', order: 4 },
27
+ { id: 'display', name: '展示组件', dir: 'display', order: 5 },
28
+ { id: 'business', name: '业务组件', dir: 'business', order: 6 },
29
+ { id: 'native', name: '原生组件', dir: 'native', order: 7 },
30
+ { id: 'api', name: 'API 插件', dir: 'api', order: 8 },
31
+ { id: 'sdk', name: 'SDK 工具库', dir: 'sdk', order: 9 },
32
+ ];
33
+
34
+ function isLimePlugin(dirName) {
35
+ return dirName.startsWith(LIME_PREFIX);
36
+ }
37
+
38
+ function getComponentName(pluginName) {
39
+ return pluginName.replace(LIME_PREFIX, '');
40
+ }
41
+
42
+ function findReadme(dirPath) {
43
+ for (const name of ['readme.md', 'README.md', 'Readme.md']) {
44
+ const filePath = join(dirPath, name);
45
+ if (existsSync(filePath)) return filePath;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function findIndexFile(dirPath) {
51
+ for (const name of ['index.ts', 'index.uts', 'index.uvue', 'index.js']) {
52
+ const filePath = join(dirPath, name);
53
+ if (existsSync(filePath)) return filePath;
54
+ }
55
+ return null;
56
+ }
57
+
58
+ function findChangelog(dirPath) {
59
+ const filePath = join(dirPath, 'changelog.md');
60
+ return existsSync(filePath) ? filePath : null;
61
+ }
62
+
63
+ function getPackageInfo(dirPath) {
64
+ const pkgPath = join(dirPath, 'package.json');
65
+ if (!existsSync(pkgPath)) return null;
66
+ try {
67
+ return JSON.parse(readFileSync(pkgPath, 'utf-8'));
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ export function scanComponents() {
74
+ const components = [];
75
+ const uniModulesPath = getUniModulesPath();
76
+
77
+ if (!existsSync(uniModulesPath)) {
78
+ return { components, errors: [{ message: `uni_modules not found at ${uniModulesPath}. Make sure you're running this command inside a uni-app project that has lime-ui-x installed.` }] };
79
+ }
80
+
81
+ const entries = readdirSync(uniModulesPath);
82
+
83
+ for (const entry of entries) {
84
+ if (!isLimePlugin(entry)) continue;
85
+
86
+ const dirPath = join(uniModulesPath, entry);
87
+ if (!statSync(dirPath).isDirectory()) continue;
88
+
89
+ try {
90
+ const componentName = getComponentName(entry);
91
+
92
+ let frontMatter = { data: {}, content: null };
93
+ const readmePath = findReadme(dirPath);
94
+ if (readmePath) {
95
+ const raw = readFileSync(readmePath, 'utf-8');
96
+ const parsed = parseFrontMatter(raw);
97
+ frontMatter = { data: parsed.data || {}, content: parsed.content || raw };
98
+ }
99
+
100
+ const pkgInfo = getPackageInfo(dirPath);
101
+
102
+ components.push({
103
+ id: componentName,
104
+ pluginName: entry,
105
+ name: frontMatter.data.name || pkgInfo?.name || componentName,
106
+ description: frontMatter.data.description || pkgInfo?.description || '',
107
+ componentTag: frontMatter.data.componentTag || `l-${componentName}`,
108
+ category: frontMatter.data.category || null,
109
+ tags: frontMatter.data.tags || [],
110
+ dependencies: frontMatter.data.dependencies || [],
111
+ version: pkgInfo?.version || null,
112
+ hasReadme: !!readmePath,
113
+ hasIndexFile: !!findIndexFile(dirPath),
114
+ hasChangelog: !!findChangelog(dirPath),
115
+ });
116
+ } catch {
117
+ // silently skip erroneous entries
118
+ }
119
+ }
120
+
121
+ components.sort((a, b) => a.pluginName.localeCompare(b.pluginName));
122
+ return { components, errors: [] };
123
+ }
124
+
125
+ export function getReadmeContent(pluginName) {
126
+ const dirPath = join(getUniModulesPath(), pluginName);
127
+ if (!existsSync(dirPath)) return null;
128
+ const readmePath = findReadme(dirPath);
129
+ if (!readmePath) return null;
130
+ try {
131
+ return readFileSync(readmePath, 'utf-8');
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ export function getChangelogContent(pluginName) {
138
+ const dirPath = join(getUniModulesPath(), pluginName);
139
+ if (!existsSync(dirPath)) return null;
140
+ const changelogPath = findChangelog(dirPath);
141
+ if (!changelogPath) return null;
142
+ try {
143
+ return readFileSync(changelogPath, 'utf-8');
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ export function getPackageJson(pluginName) {
150
+ return getPackageInfo(join(getUniModulesPath(), pluginName));
151
+ }
152
+
153
+ export function searchComponents(query) {
154
+ const { components } = scanComponents();
155
+ const q = query.toLowerCase();
156
+
157
+ return components.filter(c =>
158
+ c.name.toLowerCase().includes(q) ||
159
+ c.id.toLowerCase().includes(q) ||
160
+ c.description.toLowerCase().includes(q) ||
161
+ c.componentTag?.toLowerCase().includes(q) ||
162
+ (c.tags && c.tags.some(t => t.toLowerCase().includes(q))) ||
163
+ (c.category && c.category.toLowerCase().includes(q))
164
+ );
165
+ }
166
+
167
+ export function categorizeComponents() {
168
+ const { components } = scanComponents();
169
+
170
+ const categorized = COMPONENT_CATEGORIES.map(cat => ({
171
+ ...cat,
172
+ components: [],
173
+ }));
174
+
175
+ const categoryMap = {
176
+ '基础组件': 'basic',
177
+ '导航组件': 'nav',
178
+ '表单组件': 'form',
179
+ '反馈组件': 'feedback',
180
+ '展示组件': 'display',
181
+ '业务组件': 'business',
182
+ '原生组件': 'native',
183
+ 'API 插件': 'api',
184
+ 'SDK 工具库': 'sdk',
185
+ };
186
+
187
+ for (const comp of components) {
188
+ const catId = categoryMap[comp.category] || 'other';
189
+ const category = categorized.find(c => c.id === catId);
190
+ if (category) {
191
+ category.components.push(comp);
192
+ } else {
193
+ let other = categorized.find(c => c.id === 'other');
194
+ if (!other) {
195
+ other = { id: 'other', name: '其他', dir: 'other', order: 99, components: [] };
196
+ categorized.push(other);
197
+ }
198
+ other.components.push(comp);
199
+ }
200
+ }
201
+
202
+ return categorized.filter(c => c.components.length > 0);
203
+ }
204
+
205
+ export { COMPONENT_CATEGORIES };