tt-design-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.
- package/README.md +218 -0
- package/bin/cli.mjs +8 -0
- package/package.json +20 -0
- package/src/meta/load-meta.mjs +55 -0
- package/src/meta/resolve-package-root.mjs +31 -0
- package/src/meta/schema.mjs +107 -0
- package/src/server.mjs +149 -0
- package/src/tools/find-component-exports.mjs +39 -0
- package/src/tools/get-component-api.mjs +25 -0
- package/src/tools/get-component-style.mjs +25 -0
- package/src/tools/list-components.mjs +41 -0
- package/src/utils/component-lookup.mjs +15 -0
- package/src/utils/result.mjs +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# tt-design-mcp
|
|
2
|
+
|
|
3
|
+
面向 `tt-design` 组件库的 MCP Server,通过 npm 包分发,供支持 MCP 的 AI 客户端(如 Claude Code)查询组件元数据。
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
- 列出基础组件 / 业务组件
|
|
8
|
+
- 查询组件导出位置与推荐导入方式
|
|
9
|
+
- 查询组件 API 元信息(props、默认值、propTypes、forwardRef、memo)
|
|
10
|
+
- 查询组件样式元信息(类名、CSS 变量、主题引用)
|
|
11
|
+
|
|
12
|
+
数据来源于用户项目中已安装的 `tt-design` 包内的 `dist/meta/*.json`,无需 clone 源码仓库。
|
|
13
|
+
|
|
14
|
+
## 前置条件
|
|
15
|
+
|
|
16
|
+
1. 本机已安装 Node.js
|
|
17
|
+
2. 当前项目已安装 `tt-design`
|
|
18
|
+
3. 当前项目可安装开发依赖
|
|
19
|
+
|
|
20
|
+
## 快速接入
|
|
21
|
+
|
|
22
|
+
### 1. 安装依赖
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install tt-design
|
|
26
|
+
npm install -D tt-design-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. 配置 MCP Client
|
|
30
|
+
|
|
31
|
+
在 Claude Code 等 MCP Client 的配置中添加:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"tt-design": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "tt-design-mcp"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. 验证接入
|
|
45
|
+
|
|
46
|
+
在 Claude Code 中输入:
|
|
47
|
+
|
|
48
|
+
- `请用 tt-design MCP 列出所有基础组件`
|
|
49
|
+
- `请用 tt-design MCP 查询 Button 的 API`
|
|
50
|
+
- `请用 tt-design MCP 查询 DatePicker 的样式信息`
|
|
51
|
+
|
|
52
|
+
能正常返回结果即表示接入成功。
|
|
53
|
+
|
|
54
|
+
## 可用工具
|
|
55
|
+
|
|
56
|
+
### `list_components`
|
|
57
|
+
列出组件,支持按分类或关键字过滤。
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
输入参数:
|
|
61
|
+
category?: "basic" | "business" # 可选,按组件分类过滤
|
|
62
|
+
keyword?: string # 可选,按名称子串过滤
|
|
63
|
+
|
|
64
|
+
适用场景:查看当前有哪些组件,查找某类组件是否存在
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `find_component_exports`
|
|
68
|
+
查询组件导出位置和推荐导入方式。
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
输入参数:
|
|
72
|
+
name: string # 必填,组件名(大小写敏感)
|
|
73
|
+
|
|
74
|
+
适用场景:确认组件从哪里导出,推荐从哪个入口导入
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `get_component_api`
|
|
78
|
+
查询组件 API 元信息。
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
输入参数:
|
|
82
|
+
name: string # 必填,组件名
|
|
83
|
+
|
|
84
|
+
适用场景:查看 props、默认值、是否有 propTypes、是否使用 forwardRef/memo
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `get_component_style`
|
|
88
|
+
查询组件样式元信息。
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
输入参数:
|
|
92
|
+
name: string # 必填,组件名
|
|
93
|
+
|
|
94
|
+
适用场景:查看样式文件路径、主类名、修饰类、CSS 变量、主题引用
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 错误处理
|
|
98
|
+
|
|
99
|
+
常见错误及排查:
|
|
100
|
+
|
|
101
|
+
| 错误码 | 说明 | 排查建议 |
|
|
102
|
+
|--------|------|----------|
|
|
103
|
+
| `METADATA_LOAD_FAILED` | 未找到 `tt-design` 包 | 确认当前项目已安装 `tt-design` |
|
|
104
|
+
| `METADATA_FILE_INVALID` | 元数据文件损坏或格式不兼容 | 确认 `tt-design` 版本与 `tt-design-mcp` 兼容 |
|
|
105
|
+
| `COMPONENT_NOT_FOUND` | 组件不在元数据中 | 确认组件名大小写正确 |
|
|
106
|
+
| `INVALID_COMPONENT_NAME` | 组件名格式无效 | 组件名只能包含字母、数字、下划线、美元符 |
|
|
107
|
+
|
|
108
|
+
## 发布到 npm
|
|
109
|
+
|
|
110
|
+
### 首次发布
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
cd mcp-package
|
|
114
|
+
|
|
115
|
+
# 登录 npm(需要 npm 账号)
|
|
116
|
+
npm login
|
|
117
|
+
|
|
118
|
+
# 发布(需要先确认 package.json 中的 name、version、description 正确)
|
|
119
|
+
npm publish --access public
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 后续更新
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cd mcp-package
|
|
126
|
+
|
|
127
|
+
# 升级版本号(遵循 SemVer)
|
|
128
|
+
# patch: npm version patch
|
|
129
|
+
# minor: npm version minor
|
|
130
|
+
# major: npm version major
|
|
131
|
+
npm version patch
|
|
132
|
+
|
|
133
|
+
# 发布
|
|
134
|
+
npm publish
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 发布前检查清单
|
|
138
|
+
|
|
139
|
+
- [ ] `package.json` 中 `name` 为 `tt-design-mcp`
|
|
140
|
+
- [ ] `version` 已正确升级
|
|
141
|
+
- [ ] `description` 描述准确
|
|
142
|
+
- [ ] `engines` 指定了 Node.js 版本范围(可选)
|
|
143
|
+
- [ ] 运行测试确认正常:
|
|
144
|
+
```bash
|
|
145
|
+
npm test
|
|
146
|
+
```
|
|
147
|
+
- [ ] 在临时项目中验证完整接入流程
|
|
148
|
+
|
|
149
|
+
### 本地调试
|
|
150
|
+
|
|
151
|
+
在 `mcp-package` 目录下手动启动:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
node bin/cli.mjs
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
运行测试:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npm test
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 版本说明
|
|
164
|
+
|
|
165
|
+
- `tt-design-mcp` 与 `tt-design` 通过 `metadataSchemaVersion` 保持兼容
|
|
166
|
+
- 当前 schema 版本为 `1`
|
|
167
|
+
- 升级 `tt-design` 时会同步更新元数据,schema 不变则 `tt-design-mcp` 无需升级
|
|
168
|
+
- schema 发生不兼容变化时会同步升级 `metadataSchemaVersion`
|
|
169
|
+
|
|
170
|
+
## 本地仓库开发模式
|
|
171
|
+
|
|
172
|
+
如果你在 `tt-design` 仓库本地开发,可使用仓库内置的源码分析型 MCP(不需要安装 `tt-design-mcp`):
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"mcpServers": {
|
|
177
|
+
"tt-design": {
|
|
178
|
+
"command": "node",
|
|
179
|
+
"args": ["mcp/server.mjs"]
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
此模式直接从源码读取信息,适合调试和贡献者使用。
|
|
186
|
+
|
|
187
|
+
## 目录结构
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
mcp-package/
|
|
191
|
+
├── bin/
|
|
192
|
+
│ └── cli.mjs # CLI 入口
|
|
193
|
+
├── src/
|
|
194
|
+
│ ├── meta/
|
|
195
|
+
│ │ ├── schema.mjs # Zod schemas
|
|
196
|
+
│ │ ├── load-meta.mjs # 加载元数据
|
|
197
|
+
│ │ └── resolve-package-root.mjs # 定位 tt-design 包
|
|
198
|
+
│ ├── tools/
|
|
199
|
+
│ │ ├── list-components.mjs
|
|
200
|
+
│ │ ├── find-component-exports.mjs
|
|
201
|
+
│ │ ├── get-component-api.mjs
|
|
202
|
+
│ │ └── get-component-style.mjs
|
|
203
|
+
│ ├── utils/
|
|
204
|
+
│ │ ├── result.mjs # ok/fail 结果封装
|
|
205
|
+
│ │ └── component-lookup.mjs # 组件名校验
|
|
206
|
+
│ └── server.mjs # MCP Server 引导
|
|
207
|
+
├── test/
|
|
208
|
+
│ ├── load-meta.test.mjs # 元数据加载测试
|
|
209
|
+
│ └── server-smoke.test.mjs # 端到端冒烟测试
|
|
210
|
+
└── package.json
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 技术栈
|
|
214
|
+
|
|
215
|
+
- Node.js ESM
|
|
216
|
+
- `@modelcontextprotocol/sdk` 1.18.0
|
|
217
|
+
- Zod 3.x
|
|
218
|
+
- Node.js 内置测试(`node:test`)
|
package/bin/cli.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tt-design-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for querying tt-design component metadata",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tt-design-mcp": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test ./test/*.test.mjs"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "1.18.0",
|
|
18
|
+
"zod": "^3.23.8"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
componentsFileSchema,
|
|
6
|
+
exportsFileSchema,
|
|
7
|
+
apiFileSchema,
|
|
8
|
+
stylesFileSchema,
|
|
9
|
+
versionFileSchema,
|
|
10
|
+
} from './schema.mjs';
|
|
11
|
+
import { resolveInstalledTtDesignRoot } from './resolve-package-root.mjs';
|
|
12
|
+
|
|
13
|
+
function readJsonFile(filePath) {
|
|
14
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
15
|
+
return JSON.parse(content);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read and validate all 5 metadata JSON files from the installed tt-design package.
|
|
20
|
+
* @param {string} packageRoot - Absolute path to the tt-design package root.
|
|
21
|
+
* @returns {{ version, components, exports, api, styles }}
|
|
22
|
+
*/
|
|
23
|
+
export function loadMetaFromPackageRoot(packageRoot) {
|
|
24
|
+
const metaDir = path.join(packageRoot, 'dist/meta');
|
|
25
|
+
|
|
26
|
+
function readAndValidate(fileName, schema) {
|
|
27
|
+
try {
|
|
28
|
+
return schema.parse(readJsonFile(path.join(metaDir, fileName)));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const errorToThrow = new Error(`Failed to parse ${fileName}: ${error.message}`);
|
|
31
|
+
errorToThrow.fileName = fileName;
|
|
32
|
+
errorToThrow.schemaErrors = error.errors || [];
|
|
33
|
+
throw errorToThrow;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
version: readAndValidate('version.json', versionFileSchema),
|
|
39
|
+
components: readAndValidate('components.json', componentsFileSchema),
|
|
40
|
+
exports: readAndValidate('exports.json', exportsFileSchema),
|
|
41
|
+
api: readAndValidate('api.json', apiFileSchema),
|
|
42
|
+
styles: readAndValidate('styles.json', stylesFileSchema),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Locate the installed tt-design package and load its metadata.
|
|
48
|
+
* @param {string} [startDir] - Directory to start searching from (defaults to process.cwd()).
|
|
49
|
+
* @returns {{ packageRoot, meta }}
|
|
50
|
+
*/
|
|
51
|
+
export function loadInstalledTtDesignMeta(startDir) {
|
|
52
|
+
const packageRoot = resolveInstalledTtDesignRoot(startDir);
|
|
53
|
+
const meta = loadMetaFromPackageRoot(packageRoot);
|
|
54
|
+
return { packageRoot, meta };
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walk upward from startDir looking for node_modules/tt-design/package.json.
|
|
6
|
+
* Returns the resolved package root, or throws if not found.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveInstalledTtDesignRoot(startDir = process.cwd()) {
|
|
9
|
+
let current = path.resolve(startDir);
|
|
10
|
+
const root = path.parse(current).root;
|
|
11
|
+
|
|
12
|
+
while (current !== root) {
|
|
13
|
+
const candidate = path.join(current, 'node_modules/tt-design/package.json');
|
|
14
|
+
if (fs.existsSync(candidate)) {
|
|
15
|
+
return path.join(current, 'node_modules/tt-design');
|
|
16
|
+
}
|
|
17
|
+
current = path.dirname(current);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Last chance: check the root drive
|
|
21
|
+
const rootCandidate = path.join(root, 'node_modules/tt-design/package.json');
|
|
22
|
+
if (fs.existsSync(rootCandidate)) {
|
|
23
|
+
return path.join(root, 'node_modules/tt-design');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Could not find 'tt-design' package. ` +
|
|
28
|
+
`Make sure 'tt-design' is installed in your project.\n` +
|
|
29
|
+
`Searched from: ${path.resolve(startDir)}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// Source location embedded in each file
|
|
4
|
+
export const sourceLocationSchema = z.object({
|
|
5
|
+
file: z.string(),
|
|
6
|
+
line: z.number().nullable(),
|
|
7
|
+
column: z.number().nullable(),
|
|
8
|
+
reason: z.string().optional(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const exportedFromSchema = z.object({
|
|
12
|
+
components: z.union([z.string(), z.boolean()]),
|
|
13
|
+
business: z.union([z.string(), z.boolean()]),
|
|
14
|
+
root: z.boolean(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const componentSummarySchema = z.object({
|
|
18
|
+
name: z.string(),
|
|
19
|
+
category: z.string(),
|
|
20
|
+
sourcePath: z.string(),
|
|
21
|
+
version: z.string().nullable(),
|
|
22
|
+
exportedFrom: exportedFromSchema,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const exportEntrySchema = z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
category: z.string(),
|
|
28
|
+
sourcePath: z.string(),
|
|
29
|
+
exportedFrom: exportedFromSchema,
|
|
30
|
+
preferredImport: z
|
|
31
|
+
.object({
|
|
32
|
+
module: z.string(),
|
|
33
|
+
export: z.string(),
|
|
34
|
+
})
|
|
35
|
+
.nullable(),
|
|
36
|
+
sourceLocations: z.array(sourceLocationSchema),
|
|
37
|
+
warnings: z.array(z.string()),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const apiComponentSchema = z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
category: z.string(),
|
|
43
|
+
sourcePath: z.string(),
|
|
44
|
+
defaultExport: z.string().nullable(),
|
|
45
|
+
props: z.array(z.string()),
|
|
46
|
+
defaults: z.record(z.string(), z.unknown()),
|
|
47
|
+
subcomponents: z.array(z.string()),
|
|
48
|
+
hasDataComponentVersion: z.boolean(),
|
|
49
|
+
hasPropTypes: z.boolean(),
|
|
50
|
+
sourceLocations: z.array(sourceLocationSchema),
|
|
51
|
+
warnings: z.array(z.string()),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const styleThemeImportSchema = z.object({
|
|
55
|
+
path: z.string(),
|
|
56
|
+
kind: z.string().nullable(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const styleComponentSchema = z.object({
|
|
60
|
+
name: z.string(),
|
|
61
|
+
category: z.string(),
|
|
62
|
+
sourcePath: z.string(),
|
|
63
|
+
stylePath: z.string().nullable(),
|
|
64
|
+
blockClass: z.string().nullable(),
|
|
65
|
+
modifierClasses: z.array(z.string()),
|
|
66
|
+
relatedClasses: z.array(z.string()),
|
|
67
|
+
cssVariables: z.array(z.string()),
|
|
68
|
+
hardcodedColors: z.array(z.string()),
|
|
69
|
+
themeImport: styleThemeImportSchema.nullable(),
|
|
70
|
+
sourceLocations: z.array(sourceLocationSchema),
|
|
71
|
+
warnings: z.array(z.string()),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const versionFileSchema = z.object({
|
|
75
|
+
generatedAt: z.string(),
|
|
76
|
+
metadataSchemaVersion: z.number(),
|
|
77
|
+
packageName: z.string(),
|
|
78
|
+
packageVersion: z.string(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const componentsFileSchema = z.object({
|
|
82
|
+
generatedAt: z.string(),
|
|
83
|
+
metadataSchemaVersion: z.number(),
|
|
84
|
+
packageName: z.string(),
|
|
85
|
+
components: z.array(componentSummarySchema),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const exportsFileSchema = z.object({
|
|
89
|
+
generatedAt: z.string(),
|
|
90
|
+
metadataSchemaVersion: z.number(),
|
|
91
|
+
packageName: z.string(),
|
|
92
|
+
components: z.array(exportEntrySchema),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const apiFileSchema = z.object({
|
|
96
|
+
generatedAt: z.string(),
|
|
97
|
+
metadataSchemaVersion: z.number(),
|
|
98
|
+
packageName: z.string(),
|
|
99
|
+
components: z.array(apiComponentSchema),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const stylesFileSchema = z.object({
|
|
103
|
+
generatedAt: z.string(),
|
|
104
|
+
metadataSchemaVersion: z.number(),
|
|
105
|
+
packageName: z.string(),
|
|
106
|
+
components: z.array(styleComponentSchema),
|
|
107
|
+
});
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
import { loadInstalledTtDesignMeta } from './meta/load-meta.mjs';
|
|
9
|
+
import { listComponents } from './tools/list-components.mjs';
|
|
10
|
+
import { findComponentExports } from './tools/find-component-exports.mjs';
|
|
11
|
+
import { getComponentApi } from './tools/get-component-api.mjs';
|
|
12
|
+
import { getComponentStyle } from './tools/get-component-style.mjs';
|
|
13
|
+
import { fail, normalizeInfrastructureError } from './utils/result.mjs';
|
|
14
|
+
|
|
15
|
+
const envelopeOutputSchema = {
|
|
16
|
+
ok: z.boolean(),
|
|
17
|
+
data: z.unknown().optional(),
|
|
18
|
+
error: z
|
|
19
|
+
.object({
|
|
20
|
+
code: z.string(),
|
|
21
|
+
message: z.string(),
|
|
22
|
+
details: z.record(z.unknown()).optional(),
|
|
23
|
+
})
|
|
24
|
+
.optional(),
|
|
25
|
+
sourceLocations: z.array(
|
|
26
|
+
z.object({
|
|
27
|
+
file: z.string(),
|
|
28
|
+
line: z.number().optional(),
|
|
29
|
+
column: z.number().optional(),
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
warnings: z.array(z.string()),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function toToolResult(result) {
|
|
36
|
+
return {
|
|
37
|
+
isError: result.ok === false,
|
|
38
|
+
structuredContent: result,
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: JSON.stringify(result),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function invokeTool(handler, args) {
|
|
49
|
+
try {
|
|
50
|
+
let meta;
|
|
51
|
+
try {
|
|
52
|
+
({ meta } = loadInstalledTtDesignMeta());
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const code = error.fileName ? 'METADATA_FILE_INVALID' : 'METADATA_LOAD_FAILED';
|
|
55
|
+
return toToolResult(fail(code, error.message, {
|
|
56
|
+
...(error.fileName ? { fileName: error.fileName } : {}),
|
|
57
|
+
...(error.schemaErrors ? { schemaErrors: error.schemaErrors } : {}),
|
|
58
|
+
cwd: process.cwd(),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
return toToolResult(handler(meta, args));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return toToolResult(normalizeInfrastructureError(error));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createServer() {
|
|
68
|
+
const server = new McpServer(
|
|
69
|
+
{
|
|
70
|
+
name: 'tt-design-mcp',
|
|
71
|
+
version: '1.0.0',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
instructions:
|
|
75
|
+
'MCP server for querying tt-design component metadata. ' +
|
|
76
|
+
'Loads dist/meta/*.json from the user-installed tt-design package.',
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
server.registerTool(
|
|
81
|
+
'list_components',
|
|
82
|
+
{
|
|
83
|
+
description: 'List tt-design components with optional category and keyword filters.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
category: z.enum(['basic', 'business']).optional(),
|
|
86
|
+
keyword: z.string().min(1).optional(),
|
|
87
|
+
},
|
|
88
|
+
outputSchema: envelopeOutputSchema,
|
|
89
|
+
},
|
|
90
|
+
async (args = {}) => invokeTool(listComponents, args),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
server.registerTool(
|
|
94
|
+
'find_component_exports',
|
|
95
|
+
{
|
|
96
|
+
description: 'Find where a tt-design component is exported and how it should be imported.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
name: z.string().min(1),
|
|
99
|
+
},
|
|
100
|
+
outputSchema: envelopeOutputSchema,
|
|
101
|
+
},
|
|
102
|
+
async (args) => invokeTool(findComponentExports, args),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
server.registerTool(
|
|
106
|
+
'get_component_api',
|
|
107
|
+
{
|
|
108
|
+
description:
|
|
109
|
+
'Inspect a tt-design component source file and return normalized API metadata.',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
name: z.string().min(1),
|
|
112
|
+
},
|
|
113
|
+
outputSchema: envelopeOutputSchema,
|
|
114
|
+
},
|
|
115
|
+
async (args) => invokeTool(getComponentApi, args),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
server.registerTool(
|
|
119
|
+
'get_component_style',
|
|
120
|
+
{
|
|
121
|
+
description:
|
|
122
|
+
'Inspect a tt-design component style file and return normalized style metadata.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
name: z.string().min(1),
|
|
125
|
+
},
|
|
126
|
+
outputSchema: envelopeOutputSchema,
|
|
127
|
+
},
|
|
128
|
+
async (args) => invokeTool(getComponentStyle, args),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return server;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function startServer() {
|
|
135
|
+
const server = createServer();
|
|
136
|
+
const transport = new StdioServerTransport();
|
|
137
|
+
await server.connect(transport);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const isEntrypoint =
|
|
141
|
+
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
142
|
+
|
|
143
|
+
if (isEntrypoint) {
|
|
144
|
+
startServer().catch((error) => {
|
|
145
|
+
console.error('tt-design-mcp server failed to start');
|
|
146
|
+
console.error(error);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ok, fail } from '../utils/result.mjs';
|
|
2
|
+
import { validateComponentName } from '../utils/component-lookup.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find how a tt-design component is exported and where it should be imported from.
|
|
6
|
+
* @param {object} meta - Loaded metadata object from loadInstalledTtDesignMeta.
|
|
7
|
+
* @param {{ name: string }} args
|
|
8
|
+
* @returns {object} Result envelope.
|
|
9
|
+
*/
|
|
10
|
+
export function findComponentExports(meta, { name } = {}) {
|
|
11
|
+
const nameValidation = validateComponentName(name);
|
|
12
|
+
if (!nameValidation.valid) {
|
|
13
|
+
return fail(nameValidation.error.code, nameValidation.error.message, nameValidation.error.details);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const component = meta.exports.components.find((entry) => entry.name === name);
|
|
17
|
+
|
|
18
|
+
if (!component) {
|
|
19
|
+
return fail('COMPONENT_NOT_FOUND', 'Component not found in registry', { name });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const warnings = [...(component.warnings || [])];
|
|
23
|
+
|
|
24
|
+
if (!component.exportedFrom.root) {
|
|
25
|
+
warnings.push(`${component.name} is not exported from tt-design root (src/index.js).`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return ok(
|
|
29
|
+
{
|
|
30
|
+
name: component.name,
|
|
31
|
+
category: component.category,
|
|
32
|
+
sourcePath: component.sourcePath,
|
|
33
|
+
exportedFrom: component.exportedFrom,
|
|
34
|
+
preferredImport: component.preferredImport,
|
|
35
|
+
},
|
|
36
|
+
component.sourceLocations || [],
|
|
37
|
+
warnings,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ok, fail } from '../utils/result.mjs';
|
|
2
|
+
import { validateComponentName } from '../utils/component-lookup.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Return API metadata for a tt-design component.
|
|
6
|
+
* @param {object} meta - Loaded metadata object from loadInstalledTtDesignMeta.
|
|
7
|
+
* @param {{ name: string }} args
|
|
8
|
+
* @returns {object} Result envelope.
|
|
9
|
+
*/
|
|
10
|
+
export function getComponentApi(meta, { name } = {}) {
|
|
11
|
+
const nameValidation = validateComponentName(name);
|
|
12
|
+
if (!nameValidation.valid) {
|
|
13
|
+
return fail(nameValidation.error.code, nameValidation.error.message, nameValidation.error.details);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const component = meta.api.components.find((entry) => entry.name === name);
|
|
17
|
+
|
|
18
|
+
if (!component) {
|
|
19
|
+
return fail('COMPONENT_NOT_FOUND', 'Component not found in API metadata', { name });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { sourceLocations, warnings, ...apiData } = component;
|
|
23
|
+
|
|
24
|
+
return ok(apiData, sourceLocations || [], warnings || []);
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ok, fail } from '../utils/result.mjs';
|
|
2
|
+
import { validateComponentName } from '../utils/component-lookup.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Return style metadata for a tt-design component.
|
|
6
|
+
* @param {object} meta - Loaded metadata object from loadInstalledTtDesignMeta.
|
|
7
|
+
* @param {{ name: string }} args
|
|
8
|
+
* @returns {object} Result envelope.
|
|
9
|
+
*/
|
|
10
|
+
export function getComponentStyle(meta, { name } = {}) {
|
|
11
|
+
const nameValidation = validateComponentName(name);
|
|
12
|
+
if (!nameValidation.valid) {
|
|
13
|
+
return fail(nameValidation.error.code, nameValidation.error.message, nameValidation.error.details);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const component = meta.styles.components.find((entry) => entry.name === name);
|
|
17
|
+
|
|
18
|
+
if (!component) {
|
|
19
|
+
return fail('COMPONENT_NOT_FOUND', 'Component not found in style metadata', { name });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { sourceLocations, warnings, ...styleData } = component;
|
|
23
|
+
|
|
24
|
+
return ok(styleData, sourceLocations || [], warnings || []);
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ok, fail } from '../utils/result.mjs';
|
|
2
|
+
|
|
3
|
+
const VALID_CATEGORIES = new Set(['basic', 'business']);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List tt-design components with optional filters.
|
|
7
|
+
* @param {object} meta - Loaded metadata object from loadInstalledTtDesignMeta.
|
|
8
|
+
* @param {{ category?: string, keyword?: string }} args
|
|
9
|
+
* @returns {object} Result envelope.
|
|
10
|
+
*/
|
|
11
|
+
export function listComponents(meta, { category, keyword } = {}) {
|
|
12
|
+
if (category != null && !VALID_CATEGORIES.has(category)) {
|
|
13
|
+
return fail('INVALID_FILTER', 'category must be one of: basic, business', { category });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let components = meta.components.components;
|
|
17
|
+
|
|
18
|
+
if (category) {
|
|
19
|
+
components = components.filter((component) => component.category === category);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (keyword) {
|
|
23
|
+
components = components.filter((component) => component.name.toLowerCase().includes(keyword.toLowerCase()));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const warnings = components.flatMap((component) => component.warnings || []);
|
|
27
|
+
|
|
28
|
+
return ok(
|
|
29
|
+
{
|
|
30
|
+
components: components.map((component) => ({
|
|
31
|
+
name: component.name,
|
|
32
|
+
category: component.category,
|
|
33
|
+
version: component.version,
|
|
34
|
+
sourcePath: component.sourcePath,
|
|
35
|
+
exportedFrom: component.exportedFrom,
|
|
36
|
+
})),
|
|
37
|
+
},
|
|
38
|
+
[],
|
|
39
|
+
warnings,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const COMPONENT_NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
2
|
+
|
|
3
|
+
export function validateComponentName(name) {
|
|
4
|
+
if (!COMPONENT_NAME_PATTERN.test(name || '')) {
|
|
5
|
+
return {
|
|
6
|
+
valid: false,
|
|
7
|
+
error: {
|
|
8
|
+
code: 'INVALID_COMPONENT_NAME',
|
|
9
|
+
message: 'name must be a valid component export identifier',
|
|
10
|
+
details: { name },
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return { valid: true };
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function ok(data, sourceLocations = [], warnings = []) {
|
|
2
|
+
return {
|
|
3
|
+
ok: true,
|
|
4
|
+
data,
|
|
5
|
+
sourceLocations,
|
|
6
|
+
warnings,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function fail(code, message, details = {}) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
error: {
|
|
14
|
+
code,
|
|
15
|
+
message,
|
|
16
|
+
details,
|
|
17
|
+
},
|
|
18
|
+
sourceLocations: [],
|
|
19
|
+
warnings: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeInfrastructureError(error) {
|
|
24
|
+
return fail(
|
|
25
|
+
'INTERNAL_ERROR',
|
|
26
|
+
error?.message || 'Unexpected MCP infrastructure error',
|
|
27
|
+
);
|
|
28
|
+
}
|