lanestyle-mcp-server 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 ADDED
@@ -0,0 +1,112 @@
1
+ # LaneStyle MCP Server
2
+
3
+ 让 AI Agent 直接操作车道级图层样式配置的 MCP Server。
4
+
5
+ [![npm version](https://badge.fury.io/js/lanestyle-mcp-server.svg)](https://www.npmjs.com/package/lanestyle-mcp-server)
6
+
7
+ ## 安装
8
+
9
+ ### 方式一:npm 全局安装(推荐)
10
+
11
+ ```bash
12
+ npm install -g lanestyle-mcp-server
13
+ ```
14
+
15
+ ### 方式二:本地安装
16
+
17
+ ```bash
18
+ git clone https://github.com/your-username/lanestyle-mcp-server.git
19
+ cd lanestyle-mcp-server
20
+ npm install
21
+ npm run build
22
+ ```
23
+
24
+ ## 配置 AI 客户端
25
+
26
+ ### Qoder
27
+
28
+ 编辑 `~/.qoder/mcp.json`:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "lanestyle": {
34
+ "command": "lanestyle-mcp",
35
+ "env": {
36
+ "LANESTYLE_PROJECT_ROOT": "/path/to/your/lanestyle/project"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### Claude Desktop
44
+
45
+ 编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "lanestyle": {
51
+ "command": "lanestyle-mcp",
52
+ "env": {
53
+ "LANESTYLE_PROJECT_ROOT": "/path/to/your/lanestyle/project"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ > **注意**:`LANESTYLE_PROJECT_ROOT` 需要指向包含 `json/` 目录的项目根目录
61
+
62
+ ## 可用工具
63
+
64
+ | 工具名 | 功能 | 示例 |
65
+ |--------|------|------|
66
+ | `list_config_files` | 列出所有配置文件 | - |
67
+ | `search_fields` | 搜索字段(按名称或注释) | 搜索 "tmc" 找路况颜色 |
68
+ | `list_color_fields` | 列出所有颜色字段 | - |
69
+ | `get_field` | 获取字段值 | 获取某个颜色的当前值 |
70
+ | `set_field` | 设置字段值 | 修改颜色为 #FF0000 |
71
+ | `batch_update_colors` | 批量更新颜色 | 统一修改所有拥堵颜色 |
72
+ | `convert_color` | 颜色格式转换 | 十进制 ↔ 十六进制 |
73
+
74
+ ## 使用示例
75
+
76
+ ### 与 AI 对话
77
+
78
+ ```
79
+ 你: "把所有交通拥堵的颜色改成更深的红色"
80
+
81
+ AI 会自动:
82
+ 1. 调用 search_fields 搜索 "拥堵" 或 "Congested"
83
+ 2. 调用 get_field 查看当前值
84
+ 3. 调用 batch_update_colors 批量修改
85
+ ```
86
+
87
+ ### 颜色格式
88
+
89
+ - 输入支持: `#RRGGBB` 或 `#AARRGGBB`
90
+ - 内部存储: 32位十进制 ARGB (如 4289941584)
91
+ - Alpha 255 = 不透明,0 = 完全透明
92
+
93
+ ## 配置文件说明
94
+
95
+ | 文件名 | 描述 |
96
+ |--------|------|
97
+ | LaneRoadPolygonStyleConfig | 车道引导面样式 - TMC路况颜色等 |
98
+ | LaneDragLineStyleConfig | 牵引线样式 |
99
+ | LaneSurfaceArrowStyle | 箭头样式 |
100
+
101
+ ## 开发
102
+
103
+ ```bash
104
+ # 开发模式(热重载)
105
+ npm run dev
106
+
107
+ # 构建
108
+ npm run build
109
+
110
+ # 启动
111
+ npm start
112
+ ```
@@ -0,0 +1,102 @@
1
+ /**
2
+ * LaneStyle 配置文件工具
3
+ * 提供 JSONC 配置文件的读写、字段查询和颜色转换功能
4
+ */
5
+ export interface ARGB {
6
+ a: number;
7
+ r: number;
8
+ g: number;
9
+ b: number;
10
+ }
11
+ export interface FieldInfo {
12
+ path: string;
13
+ value: unknown;
14
+ type: string;
15
+ comment?: string;
16
+ hexColor?: string;
17
+ }
18
+ export interface ConfigFile {
19
+ name: string;
20
+ path: string;
21
+ description: string;
22
+ }
23
+ /**
24
+ * 将32位十进制ARGB转换为ARGB对象
25
+ */
26
+ export declare function decToARGB(dec: number | string): ARGB;
27
+ /**
28
+ * 将ARGB对象转换为32位十进制数字
29
+ */
30
+ export declare function argbToDec(argb: ARGB): number;
31
+ /**
32
+ * ARGB转Hex (#AARRGGBB 或 #RRGGBB)
33
+ */
34
+ export declare function argbToHex(argb: ARGB, includeAlpha?: boolean): string;
35
+ /**
36
+ * Hex转ARGB,支持 #RRGGBB 和 #AARRGGBB 格式
37
+ */
38
+ export declare function hexToARGB(hex: string): ARGB | null;
39
+ /**
40
+ * 判断数值是否可能是颜色值
41
+ * 结合字段路径名称判断,避免误判大数字
42
+ */
43
+ export declare function isLikelyColor(value: unknown, fieldPath?: string): boolean;
44
+ /**
45
+ * 移除 JSONC 中的注释,返回纯 JSON
46
+ */
47
+ export declare function stripJsonComments(jsonc: string): string;
48
+ /**
49
+ * 提取 JSONC 中的注释,返回路径到注释的映射
50
+ */
51
+ export declare function extractComments(jsonc: string): Map<string, string>;
52
+ /**
53
+ * 读取配置文件
54
+ */
55
+ export declare function readConfigFile(filePath: string): {
56
+ data: Record<string, unknown>;
57
+ comments: Map<string, string>;
58
+ rawContent: string;
59
+ };
60
+ /**
61
+ * 在 JSONC 内容中替换指定字段的值(保留注释和格式)
62
+ */
63
+ export declare function replaceValueInJsonc(content: string, fieldPath: string, newValue: unknown): string;
64
+ /**
65
+ * 保存配置文件(保留原始格式和注释)
66
+ * @param filePath 文件路径
67
+ * @param originalContent 原始文件内容
68
+ * @param modifications 修改列表 [{path, value}]
69
+ */
70
+ export declare function saveConfigFileWithComments(filePath: string, originalContent: string, modifications: Array<{
71
+ path: string;
72
+ value: unknown;
73
+ }>): void;
74
+ /**
75
+ * 保存配置文件(简单方式,会丢失注释)
76
+ * @deprecated 使用 saveConfigFileWithComments 代替
77
+ */
78
+ export declare function saveConfigFile(filePath: string, data: Record<string, unknown>): void;
79
+ /**
80
+ * 获取字段值(支持嵌套路径)
81
+ */
82
+ export declare function getFieldValue(data: Record<string, unknown>, fieldPath: string): unknown;
83
+ /**
84
+ * 设置字段值(支持嵌套路径)
85
+ */
86
+ export declare function setFieldValue(data: Record<string, unknown>, fieldPath: string, value: unknown): void;
87
+ /**
88
+ * 递归遍历所有字段
89
+ */
90
+ export declare function traverseFields(data: unknown, callback: (path: string, value: unknown, type: string) => void, prefix?: string): void;
91
+ /**
92
+ * 搜索字段(支持模糊匹配)
93
+ */
94
+ export declare function searchFields(data: Record<string, unknown>, query: string, comments: Map<string, string>): FieldInfo[];
95
+ /**
96
+ * 列出所有颜色字段
97
+ */
98
+ export declare function listColorFields(data: Record<string, unknown>, comments: Map<string, string>): FieldInfo[];
99
+ /**
100
+ * 获取可用的配置文件列表
101
+ */
102
+ export declare function listConfigFiles(baseDir: string): ConfigFile[];
@@ -0,0 +1,384 @@
1
+ /**
2
+ * LaneStyle 配置文件工具
3
+ * 提供 JSONC 配置文件的读写、字段查询和颜色转换功能
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ // ============= 颜色转换 =============
8
+ /**
9
+ * 将32位十进制ARGB转换为ARGB对象
10
+ */
11
+ export function decToARGB(dec) {
12
+ const big = BigInt(dec);
13
+ const a = Number((big >> 24n) & 255n);
14
+ const r = Number((big >> 16n) & 255n);
15
+ const g = Number((big >> 8n) & 255n);
16
+ const b = Number(big & 255n);
17
+ return { a, r, g, b };
18
+ }
19
+ /**
20
+ * 将ARGB对象转换为32位十进制数字
21
+ */
22
+ export function argbToDec(argb) {
23
+ const big = (BigInt(argb.a) << 24n) |
24
+ (BigInt(argb.r) << 16n) |
25
+ (BigInt(argb.g) << 8n) |
26
+ BigInt(argb.b);
27
+ return Number(big);
28
+ }
29
+ /**
30
+ * ARGB转Hex (#AARRGGBB 或 #RRGGBB)
31
+ */
32
+ export function argbToHex(argb, includeAlpha = true) {
33
+ const a = argb.a.toString(16).padStart(2, '0').toUpperCase();
34
+ const r = argb.r.toString(16).padStart(2, '0').toUpperCase();
35
+ const g = argb.g.toString(16).padStart(2, '0').toUpperCase();
36
+ const b = argb.b.toString(16).padStart(2, '0').toUpperCase();
37
+ return includeAlpha ? `#${a}${r}${g}${b}` : `#${r}${g}${b}`;
38
+ }
39
+ /**
40
+ * Hex转ARGB,支持 #RRGGBB 和 #AARRGGBB 格式
41
+ */
42
+ export function hexToARGB(hex) {
43
+ let cleanHex = hex.replace('#', '').toUpperCase();
44
+ if (cleanHex.length === 6) {
45
+ cleanHex = 'FF' + cleanHex; // 默认不透明
46
+ }
47
+ if (cleanHex.length !== 8) {
48
+ return null;
49
+ }
50
+ const a = parseInt(cleanHex.substring(0, 2), 16);
51
+ const r = parseInt(cleanHex.substring(2, 4), 16);
52
+ const g = parseInt(cleanHex.substring(4, 6), 16);
53
+ const b = parseInt(cleanHex.substring(6, 8), 16);
54
+ if (isNaN(a) || isNaN(r) || isNaN(g) || isNaN(b)) {
55
+ return null;
56
+ }
57
+ return { a, r, g, b };
58
+ }
59
+ /**
60
+ * 判断数值是否可能是颜色值
61
+ * 结合字段路径名称判断,避免误判大数字
62
+ */
63
+ export function isLikelyColor(value, fieldPath) {
64
+ if (typeof value !== 'number')
65
+ return false;
66
+ // 颜色值通常 > 16777215 (0xFFFFFF) 且 <= 4294967295 (0xFFFFFFFF)
67
+ const inColorRange = value > 16777215 && value <= 4294967295;
68
+ if (!inColorRange)
69
+ return false;
70
+ // 如果提供了字段路径,检查是否包含颜色相关关键词
71
+ if (fieldPath) {
72
+ const colorKeywords = ['color', 'Color', 'COLOR', 'tmc', 'Tmc', 'TMC'];
73
+ return colorKeywords.some(keyword => fieldPath.includes(keyword));
74
+ }
75
+ return inColorRange;
76
+ }
77
+ // ============= JSONC 解析 =============
78
+ /**
79
+ * 移除 JSONC 中的注释,返回纯 JSON
80
+ */
81
+ export function stripJsonComments(jsonc) {
82
+ let result = '';
83
+ let i = 0;
84
+ let inString = false;
85
+ let stringChar = '';
86
+ while (i < jsonc.length) {
87
+ const char = jsonc[i];
88
+ const nextChar = jsonc[i + 1];
89
+ if (inString) {
90
+ result += char;
91
+ if (char === '\\' && i + 1 < jsonc.length) {
92
+ result += nextChar;
93
+ i += 2;
94
+ continue;
95
+ }
96
+ if (char === stringChar) {
97
+ inString = false;
98
+ }
99
+ i++;
100
+ continue;
101
+ }
102
+ if (char === '"' || char === "'") {
103
+ inString = true;
104
+ stringChar = char;
105
+ result += char;
106
+ i++;
107
+ continue;
108
+ }
109
+ // 单行注释
110
+ if (char === '/' && nextChar === '/') {
111
+ while (i < jsonc.length && jsonc[i] !== '\n') {
112
+ i++;
113
+ }
114
+ continue;
115
+ }
116
+ // 多行注释
117
+ if (char === '/' && nextChar === '*') {
118
+ i += 2;
119
+ while (i < jsonc.length - 1 && !(jsonc[i] === '*' && jsonc[i + 1] === '/')) {
120
+ i++;
121
+ }
122
+ i += 2;
123
+ continue;
124
+ }
125
+ result += char;
126
+ i++;
127
+ }
128
+ return result;
129
+ }
130
+ /**
131
+ * 提取 JSONC 中的注释,返回路径到注释的映射
132
+ */
133
+ export function extractComments(jsonc) {
134
+ const comments = new Map();
135
+ const lines = jsonc.split('\n');
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const line = lines[i];
138
+ const commentMatch = line.match(/\/\/\s*(.+)$/);
139
+ if (commentMatch) {
140
+ // 提取该行的键名
141
+ const keyMatch = line.match(/"([^"]+)"\s*:/);
142
+ if (keyMatch) {
143
+ comments.set(keyMatch[1], commentMatch[1].trim());
144
+ }
145
+ }
146
+ }
147
+ return comments;
148
+ }
149
+ // ============= 配置文件操作 =============
150
+ /**
151
+ * 读取配置文件
152
+ */
153
+ export function readConfigFile(filePath) {
154
+ const content = fs.readFileSync(filePath, 'utf-8');
155
+ const comments = extractComments(content);
156
+ const jsonStr = stripJsonComments(content);
157
+ const data = JSON.parse(jsonStr);
158
+ return { data, comments, rawContent: content };
159
+ }
160
+ /**
161
+ * 在 JSONC 内容中替换指定字段的值(保留注释和格式)
162
+ */
163
+ export function replaceValueInJsonc(content, fieldPath, newValue) {
164
+ const parts = fieldPath.replace(/\[(\d+)\]/g, '.$1').split('.');
165
+ let result = content;
166
+ // 构建查找模式:需要找到最后一个键名对应的值
167
+ const lastKey = parts[parts.length - 1];
168
+ // 如果最后一个 part 是数字(数组索引),需要特殊处理
169
+ if (/^\d+$/.test(lastKey)) {
170
+ // 数组元素的情况比较复杂,使用简单的 JSON 替换可能更安全
171
+ // 这里我们需要找到数组中的特定元素并替换
172
+ // 暂时使用完整路径搜索
173
+ return replaceArrayElementValue(content, fieldPath, newValue);
174
+ }
175
+ // 对象属性的情况:查找 "keyName": value 并替换 value
176
+ // 创建一个正则表达式来匹配这个键及其值
177
+ const keyPattern = `"${lastKey}"\\s*:\\s*`;
178
+ // 匹配值的模式(数字、字符串、布尔值、null)
179
+ const valuePattern = `(-?\\d+\\.?\\d*|"[^"]*"|true|false|null)`;
180
+ const regex = new RegExp(`(${keyPattern})(${valuePattern})`, 'g');
181
+ // 将新值转换为 JSON 字符串
182
+ const newValueStr = typeof newValue === 'string' ? `"${newValue}"` : String(newValue);
183
+ // 替换所有匹配项(注意:如果有多个同名字段,都会被替换)
184
+ // 为了更精确,我们应该只替换特定路径的字段,但这需要更复杂的解析
185
+ // 这里简化处理,假设字段名在上下文中是唯一的
186
+ result = result.replace(regex, `$1${newValueStr}`);
187
+ return result;
188
+ }
189
+ /**
190
+ * 替换数组元素中的值
191
+ */
192
+ function replaceArrayElementValue(content, fieldPath, newValue) {
193
+ // 解析路径,找到数组和索引
194
+ const parts = fieldPath.replace(/\[(\d+)\]/g, '.$1').split('.');
195
+ // 简化处理:直接使用 JSON 解析和重新序列化的方式
196
+ // 但保留原始内容的行结构
197
+ const jsonStr = stripJsonComments(content);
198
+ const data = JSON.parse(jsonStr);
199
+ setFieldValue(data, fieldPath, newValue);
200
+ // 尝试智能替换:找到值的位置并替换
201
+ // 这里使用简单的方法:找到最后一个键名,替换其值
202
+ const lastKey = parts[parts.length - 1];
203
+ const secondLastKey = parts.length >= 2 ? parts[parts.length - 2] : null;
204
+ if (secondLastKey && /^\d+$/.test(lastKey)) {
205
+ // 最后是数组索引的情况,找到倒数第二个键
206
+ const keyPattern = `"${secondLastKey}"\\s*:\\s*`;
207
+ const valuePattern = `(-?\\d+\\.?\\d*|"[^"]*"|true|false|null)`;
208
+ const regex = new RegExp(`(${keyPattern})(${valuePattern})`, 'g');
209
+ const newValueStr = typeof newValue === 'string' ? `"${newValue}"` : String(newValue);
210
+ return content.replace(regex, `$1${newValueStr}`);
211
+ }
212
+ // 如果都找不到,退回到简单替换(可能丢失部分格式)
213
+ return content;
214
+ }
215
+ /**
216
+ * 保存配置文件(保留原始格式和注释)
217
+ * @param filePath 文件路径
218
+ * @param originalContent 原始文件内容
219
+ * @param modifications 修改列表 [{path, value}]
220
+ */
221
+ export function saveConfigFileWithComments(filePath, originalContent, modifications) {
222
+ let content = originalContent;
223
+ for (const mod of modifications) {
224
+ content = replaceValueInJsonc(content, mod.path, mod.value);
225
+ }
226
+ fs.writeFileSync(filePath, content, 'utf-8');
227
+ }
228
+ /**
229
+ * 保存配置文件(简单方式,会丢失注释)
230
+ * @deprecated 使用 saveConfigFileWithComments 代替
231
+ */
232
+ export function saveConfigFile(filePath, data) {
233
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
234
+ }
235
+ /**
236
+ * 获取字段值(支持嵌套路径)
237
+ */
238
+ export function getFieldValue(data, fieldPath) {
239
+ const parts = fieldPath.replace(/\[(\d+)\]/g, '.$1').split('.');
240
+ let current = data;
241
+ for (const part of parts) {
242
+ if (current === null || current === undefined)
243
+ return undefined;
244
+ if (typeof current === 'object') {
245
+ current = current[part];
246
+ }
247
+ else {
248
+ return undefined;
249
+ }
250
+ }
251
+ return current;
252
+ }
253
+ /**
254
+ * 设置字段值(支持嵌套路径)
255
+ */
256
+ export function setFieldValue(data, fieldPath, value) {
257
+ const parts = fieldPath.replace(/\[(\d+)\]/g, '.$1').split('.');
258
+ let current = data;
259
+ for (let i = 0; i < parts.length - 1; i++) {
260
+ const part = parts[i];
261
+ const nextPart = parts[i + 1];
262
+ const isNextPartArrayIndex = /^\d+$/.test(nextPart);
263
+ if (Array.isArray(current)) {
264
+ const index = parseInt(part, 10);
265
+ if (current[index] === undefined || current[index] === null) {
266
+ current[index] = isNextPartArrayIndex ? [] : {};
267
+ }
268
+ current = current[index];
269
+ }
270
+ else {
271
+ if (!(part in current)) {
272
+ // 根据下一个 part 是否是数字来决定创建数组还是对象
273
+ current[part] = isNextPartArrayIndex ? [] : {};
274
+ }
275
+ current = current[part];
276
+ }
277
+ }
278
+ const lastPart = parts[parts.length - 1];
279
+ if (Array.isArray(current)) {
280
+ const index = parseInt(lastPart, 10);
281
+ current[index] = value;
282
+ }
283
+ else {
284
+ current[lastPart] = value;
285
+ }
286
+ }
287
+ /**
288
+ * 递归遍历所有字段
289
+ */
290
+ export function traverseFields(data, callback, prefix = '') {
291
+ if (data === null || data === undefined)
292
+ return;
293
+ if (Array.isArray(data)) {
294
+ data.forEach((item, index) => {
295
+ const currentPath = `${prefix}[${index}]`;
296
+ traverseFields(item, callback, currentPath);
297
+ });
298
+ }
299
+ else if (typeof data === 'object') {
300
+ for (const [key, value] of Object.entries(data)) {
301
+ const currentPath = prefix ? `${prefix}.${key}` : key;
302
+ if (typeof value === 'object' && value !== null) {
303
+ traverseFields(value, callback, currentPath);
304
+ }
305
+ else {
306
+ // 传入完整路径用于颜色判断
307
+ const type = isLikelyColor(value, currentPath) ? 'color' : typeof value;
308
+ callback(currentPath, value, type);
309
+ }
310
+ }
311
+ }
312
+ }
313
+ /**
314
+ * 搜索字段(支持模糊匹配)
315
+ */
316
+ export function searchFields(data, query, comments) {
317
+ const results = [];
318
+ const queryLower = query.toLowerCase();
319
+ traverseFields(data, (path, value, type) => {
320
+ const pathLower = path.toLowerCase();
321
+ const comment = comments.get(path.split('.').pop() || '');
322
+ const commentLower = comment?.toLowerCase() || '';
323
+ if (pathLower.includes(queryLower) || commentLower.includes(queryLower)) {
324
+ const fieldInfo = {
325
+ path,
326
+ value,
327
+ type,
328
+ comment
329
+ };
330
+ if (type === 'color' && typeof value === 'number') {
331
+ fieldInfo.hexColor = argbToHex(decToARGB(value));
332
+ }
333
+ results.push(fieldInfo);
334
+ }
335
+ });
336
+ return results;
337
+ }
338
+ /**
339
+ * 列出所有颜色字段
340
+ */
341
+ export function listColorFields(data, comments) {
342
+ const results = [];
343
+ traverseFields(data, (path, value, type) => {
344
+ if (type === 'color' && typeof value === 'number') {
345
+ results.push({
346
+ path,
347
+ value,
348
+ type,
349
+ comment: comments.get(path.split('.').pop() || ''),
350
+ hexColor: argbToHex(decToARGB(value))
351
+ });
352
+ }
353
+ });
354
+ return results;
355
+ }
356
+ /**
357
+ * 获取可用的配置文件列表
358
+ */
359
+ export function listConfigFiles(baseDir) {
360
+ const jsonDir = path.join(baseDir, 'json');
361
+ const files = [];
362
+ if (fs.existsSync(jsonDir)) {
363
+ const entries = fs.readdirSync(jsonDir);
364
+ for (const entry of entries) {
365
+ if (entry.endsWith('.data')) {
366
+ const name = entry.replace('.data', '');
367
+ files.push({
368
+ name,
369
+ path: path.join(jsonDir, entry),
370
+ description: getConfigDescription(name)
371
+ });
372
+ }
373
+ }
374
+ }
375
+ return files;
376
+ }
377
+ function getConfigDescription(name) {
378
+ const descriptions = {
379
+ 'LaneRoadPolygonStyleConfig': '车道引导面样式配置 - 包含引导面颜色、TMC路况颜色等',
380
+ 'LaneDragLineStyleConfig': '牵引线样式配置 - 包含牵引线颜色、宽度等',
381
+ 'LaneSurfaceArrowStyle': '箭头样式配置 - 包含箭头颜色、尺寸等'
382
+ };
383
+ return descriptions[name] || '样式配置文件';
384
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * LaneStyle MCP Server
4
+ *
5
+ * 为 AI Agent 提供车道级图层样式编辑能力
6
+ * 支持的功能:
7
+ * - 列出配置文件
8
+ * - 搜索字段
9
+ * - 读取/修改字段值
10
+ * - 颜色转换(十进制ARGB <-> 十六进制)
11
+ * - 批量更新
12
+ */
13
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * LaneStyle MCP Server
4
+ *
5
+ * 为 AI Agent 提供车道级图层样式编辑能力
6
+ * 支持的功能:
7
+ * - 列出配置文件
8
+ * - 搜索字段
9
+ * - 读取/修改字段值
10
+ * - 颜色转换(十进制ARGB <-> 十六进制)
11
+ * - 批量更新
12
+ */
13
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
16
+ import * as path from 'path';
17
+ import * as fs from 'fs';
18
+ import { readConfigFile, saveConfigFileWithComments, getFieldValue, searchFields, listColorFields, listConfigFiles, hexToARGB, argbToDec, argbToHex, decToARGB, isLikelyColor, } from './config-tools.js';
19
+ // 项目根目录
20
+ const PROJECT_ROOT = process.env.LANESTYLE_PROJECT_ROOT || path.resolve(process.cwd(), '..');
21
+ // 创建 MCP Server
22
+ const server = new Server({
23
+ name: 'lanestyle-mcp-server',
24
+ version: '1.0.0',
25
+ }, {
26
+ capabilities: {
27
+ tools: {},
28
+ resources: {},
29
+ },
30
+ });
31
+ // ============= 工具定义 =============
32
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
33
+ return {
34
+ tools: [
35
+ {
36
+ name: 'list_config_files',
37
+ description: '列出所有可用的样式配置文件',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {},
41
+ required: [],
42
+ },
43
+ },
44
+ {
45
+ name: 'search_fields',
46
+ description: '搜索配置字段。支持按字段名或注释内容搜索。例如搜索"tmc"可找到所有路况相关颜色字段',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ config_file: {
51
+ type: 'string',
52
+ description: '配置文件名(不含扩展名),如 LaneRoadPolygonStyleConfig',
53
+ },
54
+ query: {
55
+ type: 'string',
56
+ description: '搜索关键词,支持字段名或注释内容',
57
+ },
58
+ },
59
+ required: ['config_file', 'query'],
60
+ },
61
+ },
62
+ {
63
+ name: 'list_color_fields',
64
+ description: '列出配置文件中所有颜色字段及其当前值(同时显示十进制和十六进制)',
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ config_file: {
69
+ type: 'string',
70
+ description: '配置文件名(不含扩展名)',
71
+ },
72
+ },
73
+ required: ['config_file'],
74
+ },
75
+ },
76
+ {
77
+ name: 'get_field',
78
+ description: '获取指定字段的当前值。如果是颜色值,会同时返回十进制和十六进制格式',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ config_file: {
83
+ type: 'string',
84
+ description: '配置文件名(不含扩展名)',
85
+ },
86
+ field_path: {
87
+ type: 'string',
88
+ description: '字段路径,如 surfecelinColor[0].borderTmcColorDay.tmcColorSlow',
89
+ },
90
+ },
91
+ required: ['config_file', 'field_path'],
92
+ },
93
+ },
94
+ {
95
+ name: 'set_field',
96
+ description: '设置指定字段的值。颜色值支持十六进制格式(如 #FF0000 或 #80FF0000),会自动转换为32位十进制',
97
+ inputSchema: {
98
+ type: 'object',
99
+ properties: {
100
+ config_file: {
101
+ type: 'string',
102
+ description: '配置文件名(不含扩展名)',
103
+ },
104
+ field_path: {
105
+ type: 'string',
106
+ description: '字段路径',
107
+ },
108
+ value: {
109
+ type: ['string', 'number', 'boolean'],
110
+ description: '新值。颜色可用十六进制格式(#RRGGBB 或 #AARRGGBB),数字直接输入',
111
+ },
112
+ },
113
+ required: ['config_file', 'field_path', 'value'],
114
+ },
115
+ },
116
+ {
117
+ name: 'batch_update_colors',
118
+ description: '批量更新颜色字段。适合统一修改同类颜色(如所有拥堵颜色)',
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: {
122
+ config_file: {
123
+ type: 'string',
124
+ description: '配置文件名(不含扩展名)',
125
+ },
126
+ updates: {
127
+ type: 'array',
128
+ items: {
129
+ type: 'object',
130
+ properties: {
131
+ field_path: { type: 'string' },
132
+ value: { type: 'string', description: '十六进制颜色值' },
133
+ },
134
+ required: ['field_path', 'value'],
135
+ },
136
+ description: '更新列表,每项包含 field_path 和 value',
137
+ },
138
+ },
139
+ required: ['config_file', 'updates'],
140
+ },
141
+ },
142
+ {
143
+ name: 'convert_color',
144
+ description: '颜色格式转换工具。在十进制ARGB和十六进制之间转换',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: {
148
+ value: {
149
+ type: ['string', 'number'],
150
+ description: '输入值:十进制数字(如 4289941584)或十六进制字符串(如 #FFCC0000)',
151
+ },
152
+ },
153
+ required: ['value'],
154
+ },
155
+ },
156
+ ],
157
+ };
158
+ });
159
+ // ============= 工具实现 =============
160
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
161
+ const { name, arguments: args } = request.params;
162
+ try {
163
+ switch (name) {
164
+ case 'list_config_files': {
165
+ const files = listConfigFiles(PROJECT_ROOT);
166
+ return {
167
+ content: [
168
+ {
169
+ type: 'text',
170
+ text: JSON.stringify(files, null, 2),
171
+ },
172
+ ],
173
+ };
174
+ }
175
+ case 'search_fields': {
176
+ const configFile = args?.config_file;
177
+ const query = args?.query;
178
+ // 参数验证
179
+ if (!configFile) {
180
+ throw new Error('缺少参数: config_file');
181
+ }
182
+ if (!query) {
183
+ throw new Error('缺少参数: query');
184
+ }
185
+ const filePath = path.join(PROJECT_ROOT, 'json', `${configFile}.data`);
186
+ if (!fs.existsSync(filePath)) {
187
+ throw new Error(`配置文件不存在: ${configFile}`);
188
+ }
189
+ const { data, comments } = readConfigFile(filePath);
190
+ const results = searchFields(data, query, comments);
191
+ return {
192
+ content: [
193
+ {
194
+ type: 'text',
195
+ text: `找到 ${results.length} 个匹配字段:\n${JSON.stringify(results, null, 2)}`,
196
+ },
197
+ ],
198
+ };
199
+ }
200
+ case 'list_color_fields': {
201
+ const configFile = args?.config_file;
202
+ // 参数验证
203
+ if (!configFile) {
204
+ throw new Error('缺少参数: config_file');
205
+ }
206
+ const filePath = path.join(PROJECT_ROOT, 'json', `${configFile}.data`);
207
+ if (!fs.existsSync(filePath)) {
208
+ throw new Error(`配置文件不存在: ${configFile}`);
209
+ }
210
+ const { data, comments } = readConfigFile(filePath);
211
+ const colors = listColorFields(data, comments);
212
+ return {
213
+ content: [
214
+ {
215
+ type: 'text',
216
+ text: `共 ${colors.length} 个颜色字段:\n${JSON.stringify(colors, null, 2)}`,
217
+ },
218
+ ],
219
+ };
220
+ }
221
+ case 'get_field': {
222
+ const configFile = args?.config_file;
223
+ const fieldPath = args?.field_path;
224
+ // 参数验证
225
+ if (!configFile) {
226
+ throw new Error('缺少参数: config_file');
227
+ }
228
+ if (!fieldPath) {
229
+ throw new Error('缺少参数: field_path');
230
+ }
231
+ const filePath = path.join(PROJECT_ROOT, 'json', `${configFile}.data`);
232
+ if (!fs.existsSync(filePath)) {
233
+ throw new Error(`配置文件不存在: ${configFile}`);
234
+ }
235
+ const { data, comments } = readConfigFile(filePath);
236
+ const value = getFieldValue(data, fieldPath);
237
+ if (value === undefined) {
238
+ throw new Error(`字段不存在: ${fieldPath}`);
239
+ }
240
+ const result = {
241
+ path: fieldPath,
242
+ value,
243
+ type: isLikelyColor(value, fieldPath) ? 'color' : typeof value,
244
+ comment: comments.get(fieldPath.split('.').pop() || ''),
245
+ };
246
+ if (isLikelyColor(value, fieldPath) && typeof value === 'number') {
247
+ result.hexColor = argbToHex(decToARGB(value));
248
+ }
249
+ return {
250
+ content: [
251
+ {
252
+ type: 'text',
253
+ text: JSON.stringify(result, null, 2),
254
+ },
255
+ ],
256
+ };
257
+ }
258
+ case 'set_field': {
259
+ const configFile = args?.config_file;
260
+ const fieldPath = args?.field_path;
261
+ let value = args?.value;
262
+ // 参数验证
263
+ if (!configFile) {
264
+ throw new Error('缺少参数: config_file');
265
+ }
266
+ if (!fieldPath) {
267
+ throw new Error('缺少参数: field_path');
268
+ }
269
+ if (value === undefined || value === null) {
270
+ throw new Error('缺少参数: value');
271
+ }
272
+ const filePath = path.join(PROJECT_ROOT, 'json', `${configFile}.data`);
273
+ if (!fs.existsSync(filePath)) {
274
+ throw new Error(`配置文件不存在: ${configFile}`);
275
+ }
276
+ // 如果值是十六进制颜色,转换为十进制
277
+ if (typeof value === 'string' && value.startsWith('#')) {
278
+ const argb = hexToARGB(value);
279
+ if (!argb) {
280
+ throw new Error(`无效的颜色格式: ${value}`);
281
+ }
282
+ value = argbToDec(argb);
283
+ }
284
+ const { data, rawContent } = readConfigFile(filePath);
285
+ const oldValue = getFieldValue(data, fieldPath);
286
+ // 使用保留注释的方式保存
287
+ saveConfigFileWithComments(filePath, rawContent, [{ path: fieldPath, value }]);
288
+ return {
289
+ content: [
290
+ {
291
+ type: 'text',
292
+ text: `已更新字段 ${fieldPath}:\n旧值: ${oldValue}${isLikelyColor(oldValue, fieldPath) ? ` (${argbToHex(decToARGB(oldValue))})` : ''}\n新值: ${value}${isLikelyColor(value, fieldPath) ? ` (${argbToHex(decToARGB(value))})` : ''}`,
293
+ },
294
+ ],
295
+ };
296
+ }
297
+ case 'batch_update_colors': {
298
+ const configFile = args?.config_file;
299
+ const updates = args?.updates;
300
+ // 参数验证
301
+ if (!configFile) {
302
+ throw new Error('缺少参数: config_file');
303
+ }
304
+ if (!updates || !Array.isArray(updates) || updates.length === 0) {
305
+ throw new Error('缺少参数: updates(需要非空数组)');
306
+ }
307
+ const filePath = path.join(PROJECT_ROOT, 'json', `${configFile}.data`);
308
+ if (!fs.existsSync(filePath)) {
309
+ throw new Error(`配置文件不存在: ${configFile}`);
310
+ }
311
+ const { data, rawContent } = readConfigFile(filePath);
312
+ const results = [];
313
+ const modifications = [];
314
+ for (const update of updates) {
315
+ if (!update.field_path || !update.value) {
316
+ results.push(`跳过无效项: ${JSON.stringify(update)}`);
317
+ continue;
318
+ }
319
+ const argb = hexToARGB(update.value);
320
+ if (!argb) {
321
+ results.push(`跳过 ${update.field_path}: 无效颜色格式 ${update.value}`);
322
+ continue;
323
+ }
324
+ const decValue = argbToDec(argb);
325
+ const oldValue = getFieldValue(data, update.field_path);
326
+ modifications.push({ path: update.field_path, value: decValue });
327
+ results.push(`${update.field_path}: ${oldValue} -> ${decValue} (${update.value})`);
328
+ }
329
+ // 使用保留注释的方式保存
330
+ if (modifications.length > 0) {
331
+ saveConfigFileWithComments(filePath, rawContent, modifications);
332
+ }
333
+ return {
334
+ content: [
335
+ {
336
+ type: 'text',
337
+ text: `批量更新完成 (${modifications.length}/${updates.length} 成功):\n${results.join('\n')}`,
338
+ },
339
+ ],
340
+ };
341
+ }
342
+ case 'convert_color': {
343
+ const value = args?.value;
344
+ if (typeof value === 'number') {
345
+ // 十进制转十六进制
346
+ const argb = decToARGB(value);
347
+ return {
348
+ content: [
349
+ {
350
+ type: 'text',
351
+ text: JSON.stringify({
352
+ decimal: value,
353
+ hex: argbToHex(argb),
354
+ hexNoAlpha: argbToHex(argb, false),
355
+ argb: argb,
356
+ }, null, 2),
357
+ },
358
+ ],
359
+ };
360
+ }
361
+ else if (typeof value === 'string') {
362
+ // 十六进制转十进制
363
+ const argb = hexToARGB(value);
364
+ if (!argb) {
365
+ throw new Error(`无效的颜色格式: ${value}`);
366
+ }
367
+ return {
368
+ content: [
369
+ {
370
+ type: 'text',
371
+ text: JSON.stringify({
372
+ hex: value,
373
+ decimal: argbToDec(argb),
374
+ argb: argb,
375
+ }, null, 2),
376
+ },
377
+ ],
378
+ };
379
+ }
380
+ throw new Error('请提供数字或十六进制字符串');
381
+ }
382
+ default:
383
+ throw new Error(`未知工具: ${name}`);
384
+ }
385
+ }
386
+ catch (error) {
387
+ return {
388
+ content: [
389
+ {
390
+ type: 'text',
391
+ text: `错误: ${error instanceof Error ? error.message : String(error)}`,
392
+ },
393
+ ],
394
+ isError: true,
395
+ };
396
+ }
397
+ });
398
+ // ============= 资源定义(可选,用于直接读取配置文件) =============
399
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
400
+ const files = listConfigFiles(PROJECT_ROOT);
401
+ return {
402
+ resources: files.map((file) => ({
403
+ uri: `lanestyle://config/${file.name}`,
404
+ name: file.name,
405
+ description: file.description,
406
+ mimeType: 'application/json',
407
+ })),
408
+ };
409
+ });
410
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
411
+ const uri = request.params.uri;
412
+ const match = uri.match(/^lanestyle:\/\/config\/(.+)$/);
413
+ if (!match) {
414
+ throw new Error(`无效的资源 URI: ${uri}`);
415
+ }
416
+ const configName = match[1];
417
+ const filePath = path.join(PROJECT_ROOT, 'json', `${configName}.data`);
418
+ if (!fs.existsSync(filePath)) {
419
+ throw new Error(`配置文件不存在: ${configName}`);
420
+ }
421
+ const { data } = readConfigFile(filePath);
422
+ return {
423
+ contents: [
424
+ {
425
+ uri,
426
+ mimeType: 'application/json',
427
+ text: JSON.stringify(data, null, 2),
428
+ },
429
+ ],
430
+ };
431
+ });
432
+ // ============= 启动服务器 =============
433
+ async function main() {
434
+ const transport = new StdioServerTransport();
435
+ await server.connect(transport);
436
+ console.error('LaneStyle MCP Server 已启动');
437
+ console.error(`项目目录: ${PROJECT_ROOT}`);
438
+ }
439
+ main().catch((error) => {
440
+ console.error('启动失败:', error);
441
+ process.exit(1);
442
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "lanestyle-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP Server for LaneStyle Editor - AI-powered lane style configuration",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "lanestyle-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsx src/index.ts",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "lanestyle",
24
+ "ai",
25
+ "lane-editor",
26
+ "style-config"
27
+ ],
28
+ "author": "qiquan",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/your-username/lanestyle-mcp-server"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.0.0",
42
+ "tsx": "^4.0.0",
43
+ "typescript": "^5.0.0"
44
+ }
45
+ }