@templmf/temp-solf-lmf 0.0.42 → 0.0.43
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
CHANGED
package/skill-mcp/index.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
CallToolRequestSchema,
|
|
5
|
-
ListToolsRequestSchema,
|
|
6
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
3
|
import * as fs from "fs";
|
|
8
4
|
import * as path from "path";
|
|
9
5
|
import * as os from "os";
|
|
10
6
|
import matter from "gray-matter";
|
|
7
|
+
import { z } from "zod";
|
|
11
8
|
|
|
12
9
|
// ──────────────────────────────────────────────
|
|
13
10
|
// 路径解析:支持项目级 + 全局两个 Skills 目录
|
|
@@ -16,18 +13,17 @@ import matter from "gray-matter";
|
|
|
16
13
|
function getSkillDirs(): string[] {
|
|
17
14
|
const dirs: string[] = [];
|
|
18
15
|
|
|
19
|
-
//
|
|
16
|
+
// 全局路径:~/.roo/skills(Linux/macOS)或 %USERPROFILE%\.roo\skills(Windows)
|
|
20
17
|
const globalDir = path.join(os.homedir(), ".roo", "skills");
|
|
21
18
|
if (fs.existsSync(globalDir)) dirs.push(globalDir);
|
|
22
19
|
|
|
23
|
-
// 项目路径:cwd()
|
|
20
|
+
// 项目路径:cwd()/.roo/skills
|
|
24
21
|
const projectDir = path.join(process.cwd(), ".roo", "skills");
|
|
25
22
|
if (fs.existsSync(projectDir) && projectDir !== globalDir) {
|
|
26
23
|
dirs.push(projectDir);
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
// 支持通过环境变量追加自定义路径,多个路径用分号分隔
|
|
30
|
-
// 例:SKILLS_EXTRA_DIRS=C:\my-skills;D:\shared-skills
|
|
31
27
|
if (process.env.SKILLS_EXTRA_DIRS) {
|
|
32
28
|
for (const d of process.env.SKILLS_EXTRA_DIRS.split(";")) {
|
|
33
29
|
const trimmed = d.trim();
|
|
@@ -53,7 +49,7 @@ interface SkillMeta {
|
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
// ──────────────────────────────────────────────
|
|
56
|
-
// 核心:扫描所有 Skills
|
|
52
|
+
// 核心:扫描所有 Skills(用于 list / search)
|
|
57
53
|
// ──────────────────────────────────────────────
|
|
58
54
|
|
|
59
55
|
function scanSkills(): SkillMeta[] {
|
|
@@ -61,17 +57,10 @@ function scanSkills(): SkillMeta[] {
|
|
|
61
57
|
const skillMap = new Map<string, SkillMeta>(); // name → meta,项目级覆盖全局
|
|
62
58
|
|
|
63
59
|
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
|
-
|
|
60
|
+
const source = resolveSource(baseDir);
|
|
71
61
|
if (!fs.existsSync(baseDir)) continue;
|
|
72
62
|
|
|
73
|
-
const
|
|
74
|
-
for (const entry of entries) {
|
|
63
|
+
for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
|
|
75
64
|
if (!entry.isDirectory()) continue;
|
|
76
65
|
|
|
77
66
|
const skillDir = path.join(baseDir, entry.name);
|
|
@@ -79,13 +68,10 @@ function scanSkills(): SkillMeta[] {
|
|
|
79
68
|
if (!fs.existsSync(skillFile)) continue;
|
|
80
69
|
|
|
81
70
|
try {
|
|
82
|
-
const
|
|
83
|
-
const { data } = matter(raw);
|
|
84
|
-
|
|
71
|
+
const { data } = matter(fs.readFileSync(skillFile, "utf-8"));
|
|
85
72
|
const name = (data.name as string | undefined)?.trim() ?? entry.name;
|
|
86
73
|
const description = (data.description as string | undefined)?.trim() ?? "";
|
|
87
|
-
|
|
88
|
-
// 项目级 > 全局(后扫描的覆盖先扫描的,skillDirs 里项目在后)
|
|
74
|
+
// 项目级 > 全局(后扫描的覆盖先扫描的)
|
|
89
75
|
skillMap.set(name, { name, description, skillDir, skillFile, source });
|
|
90
76
|
} catch {
|
|
91
77
|
// 解析失败跳过
|
|
@@ -93,15 +79,68 @@ function scanSkills(): SkillMeta[] {
|
|
|
93
79
|
}
|
|
94
80
|
}
|
|
95
81
|
|
|
96
|
-
return Array.from(skillMap.values()).sort((a, b) =>
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
return Array.from(skillMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ──────────────────────────────────────────────
|
|
86
|
+
// 直接按名称查找 Skill(无需全量扫描)
|
|
87
|
+
// 优先级:project > global > custom
|
|
88
|
+
// ──────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function findSkillByName(skillName: string): SkillMeta | null {
|
|
91
|
+
const skillDirs = getSkillDirs();
|
|
92
|
+
|
|
93
|
+
// 倒序遍历使项目级优先(skillDirs 里项目在后)
|
|
94
|
+
for (const baseDir of [...skillDirs].reverse()) {
|
|
95
|
+
if (!fs.existsSync(baseDir)) continue;
|
|
96
|
+
|
|
97
|
+
// 先尝试以 skillName 作为目录名直接定位
|
|
98
|
+
const directDir = path.join(baseDir, skillName);
|
|
99
|
+
const directFile = path.join(directDir, "SKILL.md");
|
|
100
|
+
if (fs.existsSync(directFile)) {
|
|
101
|
+
try {
|
|
102
|
+
const { data } = matter(fs.readFileSync(directFile, "utf-8"));
|
|
103
|
+
const name = (data.name as string | undefined)?.trim() ?? skillName;
|
|
104
|
+
const description = (data.description as string | undefined)?.trim() ?? "";
|
|
105
|
+
return { name, description, skillDir: directDir, skillFile: directFile, source: resolveSource(baseDir) };
|
|
106
|
+
} catch {
|
|
107
|
+
// 继续尝试其他路径
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 若目录名不匹配,遍历子目录对比 frontmatter name
|
|
112
|
+
for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
|
|
113
|
+
if (!entry.isDirectory()) continue;
|
|
114
|
+
const skillDir = path.join(baseDir, entry.name);
|
|
115
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
116
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const { data } = matter(fs.readFileSync(skillFile, "utf-8"));
|
|
120
|
+
const name = (data.name as string | undefined)?.trim() ?? entry.name;
|
|
121
|
+
if (name === skillName) {
|
|
122
|
+
const description = (data.description as string | undefined)?.trim() ?? "";
|
|
123
|
+
return { name, description, skillDir, skillFile, source: resolveSource(baseDir) };
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// 跳过
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
99
132
|
}
|
|
100
133
|
|
|
101
134
|
// ──────────────────────────────────────────────
|
|
102
135
|
// 工具函数
|
|
103
136
|
// ──────────────────────────────────────────────
|
|
104
137
|
|
|
138
|
+
function resolveSource(baseDir: string): string {
|
|
139
|
+
if (baseDir === path.join(os.homedir(), ".roo", "skills")) return "global";
|
|
140
|
+
if (baseDir === path.join(process.cwd(), ".roo", "skills")) return "project";
|
|
141
|
+
return "custom";
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
/** 列出所有文件(含子目录),返回相对路径列表 */
|
|
106
145
|
function listFilesRecursive(dir: string, base: string = dir): string[] {
|
|
107
146
|
const result: string[] = [];
|
|
@@ -109,11 +148,10 @@ function listFilesRecursive(dir: string, base: string = dir): string[] {
|
|
|
109
148
|
|
|
110
149
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
111
150
|
const full = path.join(dir, entry.name);
|
|
112
|
-
const rel = path.relative(base, full);
|
|
113
151
|
if (entry.isDirectory()) {
|
|
114
152
|
result.push(...listFilesRecursive(full, base));
|
|
115
153
|
} else {
|
|
116
|
-
result.push(
|
|
154
|
+
result.push(path.relative(base, full));
|
|
117
155
|
}
|
|
118
156
|
}
|
|
119
157
|
return result;
|
|
@@ -135,211 +173,157 @@ function fuzzyMatch(needle: string, haystack: string): boolean {
|
|
|
135
173
|
// MCP Server
|
|
136
174
|
// ──────────────────────────────────────────────
|
|
137
175
|
|
|
138
|
-
const server = new
|
|
139
|
-
|
|
140
|
-
|
|
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;
|
|
176
|
+
const server = new McpServer({
|
|
177
|
+
name: "skills-mcp",
|
|
178
|
+
version: "1.0.0",
|
|
179
|
+
});
|
|
204
180
|
|
|
205
|
-
|
|
206
|
-
|
|
181
|
+
// ── list_skills ──
|
|
182
|
+
server.registerTool(
|
|
183
|
+
"list_skills",
|
|
184
|
+
{
|
|
185
|
+
description: "列出所有可用的 Skills(来自全局 ~/.roo/skills 和项目 .roo/skills)",
|
|
186
|
+
inputSchema: {},
|
|
187
|
+
},
|
|
188
|
+
async () => {
|
|
207
189
|
const skills = scanSkills();
|
|
190
|
+
|
|
208
191
|
if (skills.length === 0) {
|
|
192
|
+
const dirList = getSkillDirs().map((d) => ` - ${d}`).join("\n");
|
|
209
193
|
return {
|
|
210
|
-
content: [
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
getSkillDirs().map((d) => ` - ${d}`).join("\n"),
|
|
215
|
-
},
|
|
216
|
-
],
|
|
194
|
+
content: [{
|
|
195
|
+
type: "text",
|
|
196
|
+
text: `未找到任何 Skill。\n\n请检查以下目录是否存在 SKILL.md 文件:\n${dirList}`,
|
|
197
|
+
}],
|
|
217
198
|
};
|
|
218
199
|
}
|
|
219
200
|
|
|
220
201
|
const rows = skills.map((s) => {
|
|
221
|
-
const desc = s.description.length > 80
|
|
222
|
-
? s.description.slice(0, 77) + "..."
|
|
223
|
-
: s.description;
|
|
202
|
+
const desc = s.description.length > 80 ? s.description.slice(0, 77) + "..." : s.description;
|
|
224
203
|
return `| \`${s.name}\` | ${desc} | ${s.source} |`;
|
|
225
204
|
});
|
|
226
205
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
208
|
+
type: "text",
|
|
209
|
+
text:
|
|
210
|
+
`## 可用 Skills(共 ${skills.length} 个)\n\n` +
|
|
211
|
+
`| 名称 | 描述 | 来源 |\n|------|------|------|\n` +
|
|
212
|
+
rows.join("\n"),
|
|
213
|
+
}],
|
|
214
|
+
};
|
|
234
215
|
}
|
|
216
|
+
);
|
|
235
217
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
218
|
+
// ── read_skill ──
|
|
219
|
+
server.registerTool(
|
|
220
|
+
"read_skill",
|
|
221
|
+
{
|
|
222
|
+
description: "读取指定 Skill 的完整 SKILL.md 内容",
|
|
223
|
+
inputSchema: {
|
|
224
|
+
name: z.string().describe("Skill 名称(目录名或 frontmatter 中的 name)"),
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
async ({ name: skillName }) => {
|
|
228
|
+
const skill = findSkillByName(skillName.trim());
|
|
243
229
|
|
|
244
230
|
if (!skill) {
|
|
245
|
-
const available = skills.map((s) => `\`${s.name}\``).join(", ");
|
|
246
231
|
return {
|
|
247
|
-
content: [
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
},
|
|
252
|
-
],
|
|
232
|
+
content: [{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: `未找到 Skill \`${skillName}\`。\n\n请确认名称是否正确,或使用 list_skills 查看所有可用 Skills。`,
|
|
235
|
+
}],
|
|
253
236
|
};
|
|
254
237
|
}
|
|
255
238
|
|
|
256
239
|
const content = fs.readFileSync(skill.skillFile, "utf-8");
|
|
257
240
|
return {
|
|
258
|
-
content: [
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
},
|
|
263
|
-
],
|
|
241
|
+
content: [{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: `## Skill: ${skill.name}\n**来源**: ${skill.source} (${skill.skillFile})\n\n---\n\n${content}`,
|
|
244
|
+
}],
|
|
264
245
|
};
|
|
265
246
|
}
|
|
247
|
+
);
|
|
266
248
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
249
|
+
// ── search_skills ──
|
|
250
|
+
server.registerTool(
|
|
251
|
+
"search_skills",
|
|
252
|
+
{
|
|
253
|
+
description: "模糊搜索 Skills,匹配名称或描述",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
query: z.string().describe("搜索关键词"),
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
async ({ query }) => {
|
|
270
259
|
const skills = scanSkills();
|
|
260
|
+
const q = query.trim();
|
|
271
261
|
|
|
272
262
|
const matched = skills.filter(
|
|
273
263
|
(s) =>
|
|
274
|
-
fuzzyMatch(
|
|
275
|
-
s.name.toLowerCase().includes(
|
|
276
|
-
s.description.toLowerCase().includes(
|
|
264
|
+
fuzzyMatch(q, s.name) ||
|
|
265
|
+
s.name.toLowerCase().includes(q.toLowerCase()) ||
|
|
266
|
+
s.description.toLowerCase().includes(q.toLowerCase())
|
|
277
267
|
);
|
|
278
268
|
|
|
279
269
|
if (matched.length === 0) {
|
|
280
270
|
return {
|
|
281
|
-
content: [
|
|
282
|
-
{
|
|
283
|
-
type: "text",
|
|
284
|
-
text: `未找到匹配 "${query}" 的 Skill。`,
|
|
285
|
-
},
|
|
286
|
-
],
|
|
271
|
+
content: [{ type: "text", text: `未找到匹配 "${q}" 的 Skill。` }],
|
|
287
272
|
};
|
|
288
273
|
}
|
|
289
274
|
|
|
290
275
|
const rows = matched.map((s) => {
|
|
291
|
-
const desc = s.description.length > 100
|
|
292
|
-
? s.description.slice(0, 97) + "..."
|
|
293
|
-
: s.description;
|
|
276
|
+
const desc = s.description.length > 100 ? s.description.slice(0, 97) + "..." : s.description;
|
|
294
277
|
return `| \`${s.name}\` | ${desc} | ${s.source} |`;
|
|
295
278
|
});
|
|
296
279
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
280
|
+
return {
|
|
281
|
+
content: [{
|
|
282
|
+
type: "text",
|
|
283
|
+
text:
|
|
284
|
+
`## 搜索结果:${matched.length} 个匹配 "${q}"\n\n` +
|
|
285
|
+
`| 名称 | 描述 | 来源 |\n|------|------|------|\n` +
|
|
286
|
+
rows.join("\n"),
|
|
287
|
+
}],
|
|
288
|
+
};
|
|
304
289
|
}
|
|
290
|
+
);
|
|
305
291
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
292
|
+
// ── list_skill_files ──
|
|
293
|
+
server.registerTool(
|
|
294
|
+
"list_skill_files",
|
|
295
|
+
{
|
|
296
|
+
description: "列出指定 Skill 目录下的所有附属文件(脚本、模板等)",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
name: z.string().describe("Skill 名称"),
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
async ({ name: skillName }) => {
|
|
302
|
+
const skill = findSkillByName(skillName.trim());
|
|
313
303
|
|
|
314
304
|
if (!skill) {
|
|
315
305
|
return {
|
|
316
|
-
content: [
|
|
317
|
-
{
|
|
318
|
-
type: "text",
|
|
319
|
-
text: `未找到 Skill \`${skillName}\`。`,
|
|
320
|
-
},
|
|
321
|
-
],
|
|
306
|
+
content: [{ type: "text", text: `未找到 Skill \`${skillName}\`。` }],
|
|
322
307
|
};
|
|
323
308
|
}
|
|
324
309
|
|
|
325
310
|
const files = listFilesRecursive(skill.skillDir);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
311
|
+
return {
|
|
312
|
+
content: [{
|
|
313
|
+
type: "text",
|
|
314
|
+
text:
|
|
315
|
+
`## Skill \`${skill.name}\` 的附属文件\n` +
|
|
316
|
+
`**路径**: ${skill.skillDir}\n\n` +
|
|
317
|
+
(files.length === 0 ? "_(无附属文件)_" : files.map((f) => `- \`${f}\``).join("\n")),
|
|
318
|
+
}],
|
|
319
|
+
};
|
|
334
320
|
}
|
|
321
|
+
);
|
|
335
322
|
|
|
336
|
-
|
|
337
|
-
content: [{ type: "text", text: `未知工具: ${name}` }],
|
|
338
|
-
isError: true,
|
|
339
|
-
};
|
|
340
|
-
});
|
|
341
|
-
|
|
323
|
+
// ──────────────────────────────────────────────
|
|
342
324
|
// 启动
|
|
325
|
+
// ──────────────────────────────────────────────
|
|
326
|
+
|
|
343
327
|
async function main() {
|
|
344
328
|
const transport = new StdioServerTransport();
|
|
345
329
|
await server.connect(transport);
|
|
@@ -349,4 +333,4 @@ async function main() {
|
|
|
349
333
|
main().catch((err) => {
|
|
350
334
|
console.error("Fatal error:", err);
|
|
351
335
|
process.exit(1);
|
|
352
|
-
});
|
|
336
|
+
});
|