@templmf/temp-solf-lmf 0.0.40 → 0.0.42
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/package.json +1 -1
- package/skill-mcp/README.md +74 -0
- package/skill-mcp/index.ts +352 -0
- package/skill-mcp/package (1).json +19 -0
- package/skill-mcp/tsconfig.json +16 -0
- package/files.7z +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Skills MCP
|
|
2
|
+
|
|
3
|
+
让 Roo Code 通过 MCP 工具管理和读取 Skills,彻底解决 Windows 路径环境变量问题。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. 进入项目目录
|
|
9
|
+
cd skills-mcp
|
|
10
|
+
|
|
11
|
+
# 2. 安装依赖
|
|
12
|
+
npm install
|
|
13
|
+
|
|
14
|
+
# 3. 编译 TypeScript
|
|
15
|
+
npm run build
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 配置到 Roo Code
|
|
19
|
+
|
|
20
|
+
在 Roo Code 的 MCP 配置文件中添加(`%APPDATA%\Code\User\globalStorage\rooveterinaryinc.roo-cline\settings\cline_mcp_settings.json`):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"skills-mcp": {
|
|
26
|
+
"command": "node",
|
|
27
|
+
"args": ["C:\\path\\to\\skills-mcp\\dist\\index.js"],
|
|
28
|
+
"env": {
|
|
29
|
+
"SKILLS_EXTRA_DIRS": "C:\\Users\\你的用户名\\.roo\\skills"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> `SKILLS_EXTRA_DIRS` 是可选的,用于追加额外的 Skills 目录,多个目录用分号 `;` 分隔。
|
|
37
|
+
|
|
38
|
+
## 可用工具
|
|
39
|
+
|
|
40
|
+
| 工具名 | 说明 | 参数 |
|
|
41
|
+
|--------|------|------|
|
|
42
|
+
| `list_skills` | 列出所有可用 Skills | 无 |
|
|
43
|
+
| `read_skill` | 读取指定 Skill 的完整内容 | `name`: skill名称 |
|
|
44
|
+
| `search_skills` | 模糊搜索 Skills(匹配名称或描述) | `query`: 搜索词 |
|
|
45
|
+
| `list_skill_files` | 列出 Skill 目录下的所有附属文件 | `name`: skill名称 |
|
|
46
|
+
|
|
47
|
+
## Skills 路径查找顺序
|
|
48
|
+
|
|
49
|
+
1. `%USERPROFILE%\.roo\skills\`(全局,自动解析,不再依赖环境变量字面量)
|
|
50
|
+
2. `<当前工作目录>\.roo\skills\`(项目级,优先级更高,会覆盖同名全局 Skill)
|
|
51
|
+
3. `SKILLS_EXTRA_DIRS` 环境变量中指定的路径(补充)
|
|
52
|
+
|
|
53
|
+
## 与 Slash Commands 配合使用
|
|
54
|
+
|
|
55
|
+
在 `.roo\commands\skill.md` 中:
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
---
|
|
59
|
+
description: 调用指定 Skill 的指引来完成任务
|
|
60
|
+
argument-hint: <skill-name>
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
请使用 MCP 工具 read_skill 读取名为 {参数} 的 Skill 内容,然后按其指引执行任务。
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
在 `.roo\commands\skills.md` 中:
|
|
67
|
+
|
|
68
|
+
```markdown
|
|
69
|
+
---
|
|
70
|
+
description: 列出所有可用的 Skills
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
请使用 MCP 工具 list_skills 列出所有可用的 Skills。
|
|
74
|
+
```
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import matter from "gray-matter";
|
|
11
|
+
|
|
12
|
+
// ──────────────────────────────────────────────
|
|
13
|
+
// 路径解析:支持项目级 + 全局两个 Skills 目录
|
|
14
|
+
// ──────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function getSkillDirs(): string[] {
|
|
17
|
+
const dirs: string[] = [];
|
|
18
|
+
|
|
19
|
+
// 全局路径:%USERPROFILE%\.roo\skills (Windows)或 ~/.roo/skills(Linux/macOS)
|
|
20
|
+
const globalDir = path.join(os.homedir(), ".roo", "skills");
|
|
21
|
+
if (fs.existsSync(globalDir)) dirs.push(globalDir);
|
|
22
|
+
|
|
23
|
+
// 项目路径:cwd()\.roo\skills
|
|
24
|
+
const projectDir = path.join(process.cwd(), ".roo", "skills");
|
|
25
|
+
if (fs.existsSync(projectDir) && projectDir !== globalDir) {
|
|
26
|
+
dirs.push(projectDir);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 支持通过环境变量追加自定义路径,多个路径用分号分隔
|
|
30
|
+
// 例:SKILLS_EXTRA_DIRS=C:\my-skills;D:\shared-skills
|
|
31
|
+
if (process.env.SKILLS_EXTRA_DIRS) {
|
|
32
|
+
for (const d of process.env.SKILLS_EXTRA_DIRS.split(";")) {
|
|
33
|
+
const trimmed = d.trim();
|
|
34
|
+
if (trimmed && fs.existsSync(trimmed) && !dirs.includes(trimmed)) {
|
|
35
|
+
dirs.push(trimmed);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return dirs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ──────────────────────────────────────────────
|
|
44
|
+
// Skill 数据结构
|
|
45
|
+
// ──────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
interface SkillMeta {
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
skillDir: string; // SKILL.md 所在文件夹
|
|
51
|
+
skillFile: string; // SKILL.md 完整路径
|
|
52
|
+
source: string; // "global" | "project" | 自定义路径标签
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ──────────────────────────────────────────────
|
|
56
|
+
// 核心:扫描所有 Skills
|
|
57
|
+
// ──────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function scanSkills(): SkillMeta[] {
|
|
60
|
+
const skillDirs = getSkillDirs();
|
|
61
|
+
const skillMap = new Map<string, SkillMeta>(); // name → meta,项目级覆盖全局
|
|
62
|
+
|
|
63
|
+
for (const baseDir of skillDirs) {
|
|
64
|
+
// 判断来源标签
|
|
65
|
+
const globalDir = path.join(os.homedir(), ".roo", "skills");
|
|
66
|
+
const projectDir = path.join(process.cwd(), ".roo", "skills");
|
|
67
|
+
let source = "custom";
|
|
68
|
+
if (baseDir === globalDir) source = "global";
|
|
69
|
+
else if (baseDir === projectDir) source = "project";
|
|
70
|
+
|
|
71
|
+
if (!fs.existsSync(baseDir)) continue;
|
|
72
|
+
|
|
73
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (!entry.isDirectory()) continue;
|
|
76
|
+
|
|
77
|
+
const skillDir = path.join(baseDir, entry.name);
|
|
78
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
79
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const raw = fs.readFileSync(skillFile, "utf-8");
|
|
83
|
+
const { data } = matter(raw);
|
|
84
|
+
|
|
85
|
+
const name = (data.name as string | undefined)?.trim() ?? entry.name;
|
|
86
|
+
const description = (data.description as string | undefined)?.trim() ?? "";
|
|
87
|
+
|
|
88
|
+
// 项目级 > 全局(后扫描的覆盖先扫描的,skillDirs 里项目在后)
|
|
89
|
+
skillMap.set(name, { name, description, skillDir, skillFile, source });
|
|
90
|
+
} catch {
|
|
91
|
+
// 解析失败跳过
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return Array.from(skillMap.values()).sort((a, b) =>
|
|
97
|
+
a.name.localeCompare(b.name)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ──────────────────────────────────────────────
|
|
102
|
+
// 工具函数
|
|
103
|
+
// ──────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/** 列出所有文件(含子目录),返回相对路径列表 */
|
|
106
|
+
function listFilesRecursive(dir: string, base: string = dir): string[] {
|
|
107
|
+
const result: string[] = [];
|
|
108
|
+
if (!fs.existsSync(dir)) return result;
|
|
109
|
+
|
|
110
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
111
|
+
const full = path.join(dir, entry.name);
|
|
112
|
+
const rel = path.relative(base, full);
|
|
113
|
+
if (entry.isDirectory()) {
|
|
114
|
+
result.push(...listFilesRecursive(full, base));
|
|
115
|
+
} else {
|
|
116
|
+
result.push(rel);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 模糊匹配:检查 haystack 是否包含 needle 的所有字符(按序) */
|
|
123
|
+
function fuzzyMatch(needle: string, haystack: string): boolean {
|
|
124
|
+
needle = needle.toLowerCase();
|
|
125
|
+
haystack = haystack.toLowerCase();
|
|
126
|
+
let i = 0;
|
|
127
|
+
for (const ch of haystack) {
|
|
128
|
+
if (ch === needle[i]) i++;
|
|
129
|
+
if (i === needle.length) return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ──────────────────────────────────────────────
|
|
135
|
+
// MCP Server
|
|
136
|
+
// ──────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
const server = new Server(
|
|
139
|
+
{ name: "skills-mcp", version: "1.0.0" },
|
|
140
|
+
{ capabilities: { tools: {} } }
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// 注册工具列表
|
|
144
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
145
|
+
tools: [
|
|
146
|
+
{
|
|
147
|
+
name: "list_skills",
|
|
148
|
+
description:
|
|
149
|
+
"列出所有可用的 Skills(来自全局 ~/.roo/skills 和项目 .roo/skills)",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {},
|
|
153
|
+
required: [],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "read_skill",
|
|
158
|
+
description: "读取指定 Skill 的完整 SKILL.md 内容",
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
162
|
+
name: {
|
|
163
|
+
type: "string",
|
|
164
|
+
description: "Skill 名称(目录名或 frontmatter 中的 name)",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
required: ["name"],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "search_skills",
|
|
172
|
+
description: "模糊搜索 Skills,匹配名称或描述",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
query: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "搜索关键词",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ["query"],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "list_skill_files",
|
|
186
|
+
description: "列出指定 Skill 目录下的所有附属文件(脚本、模板等)",
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
name: {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "Skill 名称",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
required: ["name"],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
// 处理工具调用
|
|
202
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
203
|
+
const { name, arguments: args } = request.params;
|
|
204
|
+
|
|
205
|
+
// ── list_skills ──
|
|
206
|
+
if (name === "list_skills") {
|
|
207
|
+
const skills = scanSkills();
|
|
208
|
+
if (skills.length === 0) {
|
|
209
|
+
return {
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: "text",
|
|
213
|
+
text: "未找到任何 Skill。\n\n请检查以下目录是否存在 SKILL.md 文件:\n" +
|
|
214
|
+
getSkillDirs().map((d) => ` - ${d}`).join("\n"),
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const rows = skills.map((s) => {
|
|
221
|
+
const desc = s.description.length > 80
|
|
222
|
+
? s.description.slice(0, 77) + "..."
|
|
223
|
+
: s.description;
|
|
224
|
+
return `| \`${s.name}\` | ${desc} | ${s.source} |`;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const text =
|
|
228
|
+
`## 可用 Skills(共 ${skills.length} 个)\n\n` +
|
|
229
|
+
`| 名称 | 描述 | 来源 |\n` +
|
|
230
|
+
`|------|------|------|\n` +
|
|
231
|
+
rows.join("\n");
|
|
232
|
+
|
|
233
|
+
return { content: [{ type: "text", text }] };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── read_skill ──
|
|
237
|
+
if (name === "read_skill") {
|
|
238
|
+
const skillName = (args as { name: string }).name.trim();
|
|
239
|
+
const skills = scanSkills();
|
|
240
|
+
const skill = skills.find(
|
|
241
|
+
(s) => s.name === skillName || s.skillDir.endsWith(path.sep + skillName)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (!skill) {
|
|
245
|
+
const available = skills.map((s) => `\`${s.name}\``).join(", ");
|
|
246
|
+
return {
|
|
247
|
+
content: [
|
|
248
|
+
{
|
|
249
|
+
type: "text",
|
|
250
|
+
text: `未找到 Skill \`${skillName}\`。\n\n可用的 Skills:${available || "(无)"}`,
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const content = fs.readFileSync(skill.skillFile, "utf-8");
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: `## Skill: ${skill.name}\n**来源**: ${skill.source} (${skill.skillFile})\n\n---\n\n${content}`,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── search_skills ──
|
|
268
|
+
if (name === "search_skills") {
|
|
269
|
+
const query = (args as { query: string }).query.trim();
|
|
270
|
+
const skills = scanSkills();
|
|
271
|
+
|
|
272
|
+
const matched = skills.filter(
|
|
273
|
+
(s) =>
|
|
274
|
+
fuzzyMatch(query, s.name) ||
|
|
275
|
+
s.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
276
|
+
s.description.toLowerCase().includes(query.toLowerCase())
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (matched.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: "text",
|
|
284
|
+
text: `未找到匹配 "${query}" 的 Skill。`,
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const rows = matched.map((s) => {
|
|
291
|
+
const desc = s.description.length > 100
|
|
292
|
+
? s.description.slice(0, 97) + "..."
|
|
293
|
+
: s.description;
|
|
294
|
+
return `| \`${s.name}\` | ${desc} | ${s.source} |`;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const text =
|
|
298
|
+
`## 搜索结果:${matched.length} 个匹配 "${query}"\n\n` +
|
|
299
|
+
`| 名称 | 描述 | 来源 |\n` +
|
|
300
|
+
`|------|------|------|\n` +
|
|
301
|
+
rows.join("\n");
|
|
302
|
+
|
|
303
|
+
return { content: [{ type: "text", text }] };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── list_skill_files ──
|
|
307
|
+
if (name === "list_skill_files") {
|
|
308
|
+
const skillName = (args as { name: string }).name.trim();
|
|
309
|
+
const skills = scanSkills();
|
|
310
|
+
const skill = skills.find(
|
|
311
|
+
(s) => s.name === skillName || s.skillDir.endsWith(path.sep + skillName)
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (!skill) {
|
|
315
|
+
return {
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: `未找到 Skill \`${skillName}\`。`,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const files = listFilesRecursive(skill.skillDir);
|
|
326
|
+
const text =
|
|
327
|
+
`## Skill \`${skill.name}\` 的附属文件\n` +
|
|
328
|
+
`**路径**: ${skill.skillDir}\n\n` +
|
|
329
|
+
(files.length === 0
|
|
330
|
+
? "_(无附属文件)_"
|
|
331
|
+
: files.map((f) => `- \`${f}\``).join("\n"));
|
|
332
|
+
|
|
333
|
+
return { content: [{ type: "text", text }] };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
content: [{ type: "text", text: `未知工具: ${name}` }],
|
|
338
|
+
isError: true,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// 启动
|
|
343
|
+
async function main() {
|
|
344
|
+
const transport = new StdioServerTransport();
|
|
345
|
+
await server.connect(transport);
|
|
346
|
+
console.error("Skills MCP server started");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
main().catch((err) => {
|
|
350
|
+
console.error("Fatal error:", err);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skills-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for managing and reading Roo Code Skills",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"start": "node dist/index.js",
|
|
9
|
+
"dev": "ts-node src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
13
|
+
"gray-matter": "^4.0.3"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^20.0.0",
|
|
17
|
+
"typescript": "^5.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|
package/files.7z
DELETED
|
Binary file
|