postar-pipe-mcp 0.0.2 → 1.0.1
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/dist/server.js +465 -55
- package/dist/skills/cd/SKILL.md +77 -239
- package/dist/skills/cd/steps.yaml +378 -0
- package/dist/skills/cd/template.yaml +67 -4
- package/dist/skills/ci/SKILL.md +253 -125
- package/dist/skills/ci/steps.yaml +504 -0
- package/dist/skills/ci/template.yaml +70 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ZodOptional as ZodOptional$2, z } from "zod";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import require$$0, { existsSync, readdirSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
import require$$0$1, { resolve as resolve$3, dirname, join } from "path";
|
|
4
7
|
import require$$0$2 from "child_process";
|
|
5
|
-
import require$$0$1, { dirname, resolve as resolve$3 } from "path";
|
|
6
|
-
import require$$0, { existsSync, readdirSync, readFileSync } from "fs";
|
|
7
8
|
import process$1 from "node:process";
|
|
8
9
|
import { PassThrough } from "node:stream";
|
|
9
10
|
import { fileURLToPath } from "url";
|
|
@@ -21101,12 +21102,14 @@ async function getProxiedTools() {
|
|
|
21101
21102
|
try {
|
|
21102
21103
|
const tools = await manager2.listTools(mainJenkins);
|
|
21103
21104
|
for (const tool of tools.tools || []) {
|
|
21105
|
+
const proxiedToolName = `${prefix}${tool.name}`;
|
|
21104
21106
|
const proxiedTool = {
|
|
21105
21107
|
...tool,
|
|
21106
|
-
name:
|
|
21108
|
+
name: proxiedToolName,
|
|
21107
21109
|
description: `[${mainJenkins}] ${tool.description || ""}`
|
|
21108
21110
|
};
|
|
21109
21111
|
allTools.push(proxiedTool);
|
|
21112
|
+
toolSchemaCache.set(proxiedToolName, tool.inputSchema);
|
|
21110
21113
|
}
|
|
21111
21114
|
console.error(`[MCP-PIPE] 已代理 ${mainJenkins} 的 ${tools.tools?.length || 0} 个工具`);
|
|
21112
21115
|
} catch (error2) {
|
|
@@ -21119,12 +21122,14 @@ async function getProxiedTools() {
|
|
|
21119
21122
|
const jenkinsTools = allTools.filter((t) => t.name.startsWith(sourcePrefix));
|
|
21120
21123
|
for (const tool of jenkinsTools) {
|
|
21121
21124
|
const originalToolName = tool.name.slice(sourcePrefix.length);
|
|
21125
|
+
const proxiedToolName = `${prefix}${originalToolName}`;
|
|
21122
21126
|
const proxiedTool = {
|
|
21123
21127
|
...tool,
|
|
21124
|
-
name:
|
|
21128
|
+
name: proxiedToolName,
|
|
21125
21129
|
description: `[${serverName}] ${tool.description || ""}`
|
|
21126
21130
|
};
|
|
21127
21131
|
allTools.push(proxiedTool);
|
|
21132
|
+
toolSchemaCache.set(proxiedToolName, tool.inputSchema);
|
|
21128
21133
|
}
|
|
21129
21134
|
console.error(`[MCP-PIPE] 已为 ${serverName} 生成 ${jenkinsTools.length} 个工具别名`);
|
|
21130
21135
|
}
|
|
@@ -21133,12 +21138,14 @@ async function getProxiedTools() {
|
|
|
21133
21138
|
try {
|
|
21134
21139
|
const tools = await manager2.listTools(serverName);
|
|
21135
21140
|
for (const tool of tools.tools || []) {
|
|
21141
|
+
const proxiedToolName = `${prefix}${tool.name}`;
|
|
21136
21142
|
const proxiedTool = {
|
|
21137
21143
|
...tool,
|
|
21138
|
-
name:
|
|
21144
|
+
name: proxiedToolName,
|
|
21139
21145
|
description: `[${serverName}] ${tool.description || ""}`
|
|
21140
21146
|
};
|
|
21141
21147
|
allTools.push(proxiedTool);
|
|
21148
|
+
toolSchemaCache.set(proxiedToolName, tool.inputSchema);
|
|
21142
21149
|
}
|
|
21143
21150
|
console.error(`[MCP-PIPE] 已代理 ${serverName} 的 ${tools.tools?.length || 0} 个工具`);
|
|
21144
21151
|
} catch (error2) {
|
|
@@ -21147,9 +21154,40 @@ async function getProxiedTools() {
|
|
|
21147
21154
|
}
|
|
21148
21155
|
return allTools;
|
|
21149
21156
|
}
|
|
21157
|
+
function fixArgumentTypes(args, toolSchema) {
|
|
21158
|
+
if (!args || typeof args !== "object") return args;
|
|
21159
|
+
const fixedArgs = { ...args };
|
|
21160
|
+
if (!toolSchema?.properties) {
|
|
21161
|
+
console.warn(`[MCP-PIPE] 警告: 工具缺少 schema,跳过类型修复`);
|
|
21162
|
+
return fixedArgs;
|
|
21163
|
+
}
|
|
21164
|
+
for (const [key, schema] of Object.entries(toolSchema.properties)) {
|
|
21165
|
+
if (key in fixedArgs && typeof fixedArgs[key] === "string") {
|
|
21166
|
+
const propSchema = schema;
|
|
21167
|
+
if (propSchema.type === "number" || propSchema.type === "integer") {
|
|
21168
|
+
const num = Number(fixedArgs[key]);
|
|
21169
|
+
if (!isNaN(num)) {
|
|
21170
|
+
fixedArgs[key] = num;
|
|
21171
|
+
console.debug(`[MCP-PIPE] 类型转换: ${key} "${fixedArgs[key]}" → ${num} (number)`);
|
|
21172
|
+
}
|
|
21173
|
+
}
|
|
21174
|
+
if (propSchema.type === "boolean") {
|
|
21175
|
+
const str = fixedArgs[key].toLowerCase();
|
|
21176
|
+
if (str === "true" || str === "false") {
|
|
21177
|
+
fixedArgs[key] = str === "true";
|
|
21178
|
+
console.debug(`[MCP-PIPE] 类型转换: ${key} "${str}" → ${fixedArgs[key]} (boolean)`);
|
|
21179
|
+
}
|
|
21180
|
+
}
|
|
21181
|
+
}
|
|
21182
|
+
}
|
|
21183
|
+
return fixedArgs;
|
|
21184
|
+
}
|
|
21185
|
+
const toolSchemaCache = /* @__PURE__ */ new Map();
|
|
21150
21186
|
async function handleProxiedTool(name, args) {
|
|
21151
21187
|
const manager2 = getMCPClientManager();
|
|
21152
21188
|
const servers = getRegisteredServers();
|
|
21189
|
+
const toolSchema = toolSchemaCache.get(name);
|
|
21190
|
+
const fixedArgs = fixArgumentTypes(args, toolSchema);
|
|
21153
21191
|
if (name.startsWith("mcp_jenkins_")) {
|
|
21154
21192
|
let targetMCP = null;
|
|
21155
21193
|
const otherJenkinsServers = servers.filter((s) => s.startsWith("jenkins-"));
|
|
@@ -21167,7 +21205,7 @@ async function handleProxiedTool(name, args) {
|
|
|
21167
21205
|
const prefix = getToolPrefix(targetMCP);
|
|
21168
21206
|
const originalToolName = name.slice(prefix.length);
|
|
21169
21207
|
try {
|
|
21170
|
-
const result = await manager2.callTool(targetMCP, originalToolName,
|
|
21208
|
+
const result = await manager2.callTool(targetMCP, originalToolName, fixedArgs);
|
|
21171
21209
|
return result;
|
|
21172
21210
|
} catch (error2) {
|
|
21173
21211
|
return {
|
|
@@ -21182,7 +21220,7 @@ async function handleProxiedTool(name, args) {
|
|
|
21182
21220
|
if (name.startsWith(prefix)) {
|
|
21183
21221
|
const originalToolName = name.slice(prefix.length);
|
|
21184
21222
|
try {
|
|
21185
|
-
const result = await manager2.callTool(serverName, originalToolName,
|
|
21223
|
+
const result = await manager2.callTool(serverName, originalToolName, fixedArgs);
|
|
21186
21224
|
return result;
|
|
21187
21225
|
} catch (error2) {
|
|
21188
21226
|
return {
|
|
@@ -21197,15 +21235,88 @@ async function handleProxiedTool(name, args) {
|
|
|
21197
21235
|
const skillCache = /* @__PURE__ */ new Map();
|
|
21198
21236
|
const skillMetadataCache = /* @__PURE__ */ new Map();
|
|
21199
21237
|
let remoteSkillsList = null;
|
|
21238
|
+
function parseSkillSource(url, index) {
|
|
21239
|
+
const questionMarkIndex = url.indexOf("?");
|
|
21240
|
+
let cleanUrl = url;
|
|
21241
|
+
let skillsList;
|
|
21242
|
+
if (questionMarkIndex !== -1) {
|
|
21243
|
+
cleanUrl = url.substring(0, questionMarkIndex);
|
|
21244
|
+
const queryString = url.substring(questionMarkIndex + 1);
|
|
21245
|
+
const params = new URLSearchParams(queryString);
|
|
21246
|
+
const skillsParam = params.get("skills");
|
|
21247
|
+
if (skillsParam) {
|
|
21248
|
+
skillsList = skillsParam.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
21249
|
+
}
|
|
21250
|
+
}
|
|
21251
|
+
const rawUrl = cleanUrl.replace("/tree/", "/raw/").replace(/\/$/, "");
|
|
21252
|
+
const match = cleanUrl.match(/\/([^\/]+)\/[^\/]+\/tree\//);
|
|
21253
|
+
const name = match ? match[1] : `source-${index}`;
|
|
21254
|
+
return {
|
|
21255
|
+
name,
|
|
21256
|
+
baseUrl: rawUrl,
|
|
21257
|
+
originalUrl: cleanUrl,
|
|
21258
|
+
skillsList
|
|
21259
|
+
};
|
|
21260
|
+
}
|
|
21261
|
+
function parseCommandLineArgs() {
|
|
21262
|
+
const result = {};
|
|
21263
|
+
const skillUrls = [];
|
|
21264
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
21265
|
+
const arg = process.argv[i];
|
|
21266
|
+
if (arg === "--skill-url" && i + 1 < process.argv.length) {
|
|
21267
|
+
skillUrls.push(process.argv[i + 1]);
|
|
21268
|
+
} else if (arg.startsWith("--skill-url=")) {
|
|
21269
|
+
skillUrls.push(arg.split("=")[1]);
|
|
21270
|
+
}
|
|
21271
|
+
if (arg.startsWith("--skills-urls=")) {
|
|
21272
|
+
result.skillsUrls = arg.split("=")[1];
|
|
21273
|
+
} else if (arg === "--skills-urls" && i + 1 < process.argv.length) {
|
|
21274
|
+
result.skillsUrls = process.argv[i + 1];
|
|
21275
|
+
}
|
|
21276
|
+
if (arg.startsWith("--skills-url=")) {
|
|
21277
|
+
result.skillsUrl = arg.split("=")[1];
|
|
21278
|
+
} else if (arg === "--skills-url" && i + 1 < process.argv.length) {
|
|
21279
|
+
result.skillsUrl = process.argv[i + 1];
|
|
21280
|
+
}
|
|
21281
|
+
}
|
|
21282
|
+
if (skillUrls.length > 0) {
|
|
21283
|
+
result.skillUrls = skillUrls;
|
|
21284
|
+
}
|
|
21285
|
+
return result;
|
|
21286
|
+
}
|
|
21200
21287
|
function getSkillsConfig() {
|
|
21201
|
-
const
|
|
21288
|
+
const cliArgs = parseCommandLineArgs();
|
|
21289
|
+
if (cliArgs.skillUrls && cliArgs.skillUrls.length > 0) {
|
|
21290
|
+
const sources = cliArgs.skillUrls.map((url, index) => parseSkillSource(url.trim(), index)).filter((source) => source.originalUrl.length > 0);
|
|
21291
|
+
if (sources.length > 0) {
|
|
21292
|
+
return {
|
|
21293
|
+
source: "remote",
|
|
21294
|
+
sources,
|
|
21295
|
+
baseUrl: sources[0].baseUrl,
|
|
21296
|
+
originalUrl: sources[0].originalUrl
|
|
21297
|
+
};
|
|
21298
|
+
}
|
|
21299
|
+
}
|
|
21300
|
+
const skillsUrls = cliArgs.skillsUrls || process.env.SKILLS_URLS;
|
|
21301
|
+
if (skillsUrls) {
|
|
21302
|
+
const sources = skillsUrls.split(",").map((url, index) => parseSkillSource(url.trim(), index)).filter((source) => source.originalUrl.length > 0);
|
|
21303
|
+
if (sources.length > 0) {
|
|
21304
|
+
return {
|
|
21305
|
+
source: "remote",
|
|
21306
|
+
sources,
|
|
21307
|
+
baseUrl: sources[0].baseUrl,
|
|
21308
|
+
originalUrl: sources[0].originalUrl
|
|
21309
|
+
};
|
|
21310
|
+
}
|
|
21311
|
+
}
|
|
21312
|
+
const skillsUrl = cliArgs.skillsUrl || process.env.SKILLS_URL;
|
|
21202
21313
|
if (skillsUrl) {
|
|
21203
|
-
const
|
|
21314
|
+
const source = parseSkillSource(skillsUrl, 0);
|
|
21204
21315
|
return {
|
|
21205
21316
|
source: "remote",
|
|
21206
|
-
|
|
21207
|
-
|
|
21208
|
-
|
|
21317
|
+
sources: [source],
|
|
21318
|
+
baseUrl: source.baseUrl,
|
|
21319
|
+
originalUrl: source.originalUrl
|
|
21209
21320
|
};
|
|
21210
21321
|
}
|
|
21211
21322
|
return {
|
|
@@ -21283,21 +21394,69 @@ async function fetchRemoteSkillsFromDirectory(config2) {
|
|
|
21283
21394
|
return [];
|
|
21284
21395
|
}
|
|
21285
21396
|
}
|
|
21397
|
+
const skillSourceMap = /* @__PURE__ */ new Map();
|
|
21398
|
+
const CACHE_TTL_MINUTES = process.env.SKILLS_CACHE_TTL ? parseInt(process.env.SKILLS_CACHE_TTL) : 30;
|
|
21399
|
+
const CACHE_TTL = CACHE_TTL_MINUTES * 60 * 1e3;
|
|
21400
|
+
function isCacheExpired(timestamp) {
|
|
21401
|
+
return Date.now() - timestamp > CACHE_TTL;
|
|
21402
|
+
}
|
|
21286
21403
|
async function fetchRemoteSkillsList(config2) {
|
|
21287
|
-
if (remoteSkillsList) {
|
|
21288
|
-
return remoteSkillsList;
|
|
21404
|
+
if (remoteSkillsList && !isCacheExpired(remoteSkillsList.timestamp)) {
|
|
21405
|
+
return remoteSkillsList.data;
|
|
21289
21406
|
}
|
|
21407
|
+
console.error(`[SKILL-LOADER] Skills 列表缓存过期或不存在,重新加载...`);
|
|
21290
21408
|
const skillsListEnv = process.env.SKILLS_LIST;
|
|
21291
21409
|
if (skillsListEnv) {
|
|
21292
|
-
const
|
|
21293
|
-
remoteSkillsList =
|
|
21294
|
-
console.error(`[SKILL-LOADER] 从环境变量加载 skills: ${
|
|
21295
|
-
return
|
|
21410
|
+
const skills = skillsListEnv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
21411
|
+
remoteSkillsList = { data: skills, timestamp: Date.now() };
|
|
21412
|
+
console.error(`[SKILL-LOADER] 从环境变量加载 skills: ${skills.join(", ")}`);
|
|
21413
|
+
return skills;
|
|
21414
|
+
}
|
|
21415
|
+
console.error(`[SKILL-LOADER] 未配置 SKILLS_LIST,检查各源独立配置...`);
|
|
21416
|
+
const allSkills = [];
|
|
21417
|
+
skillSourceMap.clear();
|
|
21418
|
+
if (config2.sources && config2.sources.length > 0) {
|
|
21419
|
+
for (const source of config2.sources) {
|
|
21420
|
+
console.error(`[SKILL-LOADER] 扫描源: ${source.name} (${source.originalUrl})`);
|
|
21421
|
+
if (source.skillsList && source.skillsList.length > 0) {
|
|
21422
|
+
console.error(`[SKILL-LOADER] 源 ${source.name} 使用独立 skills 列表: ${source.skillsList.join(", ")}`);
|
|
21423
|
+
for (const skillName of source.skillsList) {
|
|
21424
|
+
if (!allSkills.includes(skillName)) {
|
|
21425
|
+
allSkills.push(skillName);
|
|
21426
|
+
}
|
|
21427
|
+
skillSourceMap.set(skillName, source);
|
|
21428
|
+
}
|
|
21429
|
+
} else {
|
|
21430
|
+
console.error(`[SKILL-LOADER] 源 ${source.name} 未配置独立 skills,自动遍历目录...`);
|
|
21431
|
+
const sourceConfig = {
|
|
21432
|
+
source: "remote",
|
|
21433
|
+
baseUrl: source.baseUrl,
|
|
21434
|
+
originalUrl: source.originalUrl
|
|
21435
|
+
};
|
|
21436
|
+
const skills = await fetchRemoteSkillsFromDirectory(sourceConfig);
|
|
21437
|
+
console.error(`[SKILL-LOADER] 源 ${source.name} 发现 skills: ${skills.join(", ")}`);
|
|
21438
|
+
for (const skillName of skills) {
|
|
21439
|
+
if (!allSkills.includes(skillName)) {
|
|
21440
|
+
allSkills.push(skillName);
|
|
21441
|
+
}
|
|
21442
|
+
skillSourceMap.set(skillName, source);
|
|
21443
|
+
}
|
|
21444
|
+
}
|
|
21445
|
+
}
|
|
21446
|
+
} else if (config2.baseUrl && config2.originalUrl) {
|
|
21447
|
+
const skills = await fetchRemoteSkillsFromDirectory(config2);
|
|
21448
|
+
for (const skillName of skills) {
|
|
21449
|
+
allSkills.push(skillName);
|
|
21450
|
+
skillSourceMap.set(skillName, {
|
|
21451
|
+
name: "default",
|
|
21452
|
+
baseUrl: config2.baseUrl,
|
|
21453
|
+
originalUrl: config2.originalUrl
|
|
21454
|
+
});
|
|
21455
|
+
}
|
|
21296
21456
|
}
|
|
21297
|
-
console.error(`[SKILL-LOADER]
|
|
21298
|
-
|
|
21299
|
-
|
|
21300
|
-
return skills;
|
|
21457
|
+
console.error(`[SKILL-LOADER] 汇总所有 skills: ${allSkills.join(", ")}`);
|
|
21458
|
+
remoteSkillsList = { data: allSkills, timestamp: Date.now() };
|
|
21459
|
+
return allSkills;
|
|
21301
21460
|
}
|
|
21302
21461
|
async function loadRemoteSkillContent(skillName, baseUrl) {
|
|
21303
21462
|
const skillUrl = `${baseUrl}/${skillName}/SKILL.md`;
|
|
@@ -21357,8 +21516,20 @@ async function getAvailableSkills() {
|
|
|
21357
21516
|
}
|
|
21358
21517
|
async function loadSkillContent(skillName) {
|
|
21359
21518
|
const config2 = getSkillsConfig();
|
|
21360
|
-
if (config2.source === "remote"
|
|
21361
|
-
|
|
21519
|
+
if (config2.source === "remote") {
|
|
21520
|
+
const source = skillSourceMap.get(skillName);
|
|
21521
|
+
if (source) {
|
|
21522
|
+
console.error(`[SKILL-LOADER] 从源 ${source.name} 加载 skill: ${skillName}`);
|
|
21523
|
+
return loadRemoteSkillContent(skillName, source.baseUrl);
|
|
21524
|
+
}
|
|
21525
|
+
if (config2.sources && config2.sources.length > 0) {
|
|
21526
|
+
const firstSource = config2.sources[0];
|
|
21527
|
+
console.error(`[SKILL-LOADER] 未找到来源映射,使用默认源 ${firstSource.name} 加载: ${skillName}`);
|
|
21528
|
+
return loadRemoteSkillContent(skillName, firstSource.baseUrl);
|
|
21529
|
+
}
|
|
21530
|
+
if (config2.baseUrl) {
|
|
21531
|
+
return loadRemoteSkillContent(skillName, config2.baseUrl);
|
|
21532
|
+
}
|
|
21362
21533
|
}
|
|
21363
21534
|
const skillPath = resolve$3(getSkillsDir(), skillName, "SKILL.md");
|
|
21364
21535
|
try {
|
|
@@ -21370,13 +21541,16 @@ Skill 文件加载失败,请检查安装。`;
|
|
|
21370
21541
|
}
|
|
21371
21542
|
}
|
|
21372
21543
|
async function getSkillContent(skillName) {
|
|
21373
|
-
|
|
21374
|
-
|
|
21375
|
-
|
|
21544
|
+
const cached2 = skillCache.get(skillName);
|
|
21545
|
+
if (cached2 && !isCacheExpired(cached2.timestamp)) {
|
|
21546
|
+
return cached2.data;
|
|
21376
21547
|
}
|
|
21377
|
-
|
|
21548
|
+
console.error(`[SKILL-LOADER] Skill 缓存过期或不存在,重新加载: ${skillName}`);
|
|
21549
|
+
const content = await loadSkillContent(skillName);
|
|
21550
|
+
skillCache.set(skillName, { data: content, timestamp: Date.now() });
|
|
21551
|
+
return content;
|
|
21378
21552
|
}
|
|
21379
|
-
function parseFrontmatter(content) {
|
|
21553
|
+
async function parseFrontmatter(content) {
|
|
21380
21554
|
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
21381
21555
|
const match = content.match(frontmatterRegex);
|
|
21382
21556
|
if (!match) {
|
|
@@ -21384,41 +21558,229 @@ function parseFrontmatter(content) {
|
|
|
21384
21558
|
}
|
|
21385
21559
|
const frontmatterText = match[1];
|
|
21386
21560
|
const body = match[2];
|
|
21387
|
-
|
|
21388
|
-
|
|
21389
|
-
|
|
21390
|
-
|
|
21391
|
-
|
|
21392
|
-
|
|
21393
|
-
|
|
21394
|
-
|
|
21395
|
-
|
|
21561
|
+
try {
|
|
21562
|
+
const yaml = await import("js-yaml");
|
|
21563
|
+
const metadata2 = yaml.load(frontmatterText) || {};
|
|
21564
|
+
return { metadata: metadata2, body };
|
|
21565
|
+
} catch {
|
|
21566
|
+
const metadata2 = {};
|
|
21567
|
+
const lines = frontmatterText.split("\n");
|
|
21568
|
+
for (const line of lines) {
|
|
21569
|
+
const colonIndex = line.indexOf(":");
|
|
21570
|
+
if (colonIndex > 0 && !line.startsWith("-") && !line.startsWith(" ")) {
|
|
21571
|
+
const key = line.slice(0, colonIndex).trim();
|
|
21572
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
21573
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
21574
|
+
value = value.slice(1, -1);
|
|
21575
|
+
}
|
|
21576
|
+
metadata2[key] = value;
|
|
21396
21577
|
}
|
|
21397
|
-
metadata2[key] = value;
|
|
21398
21578
|
}
|
|
21579
|
+
return { metadata: metadata2, body };
|
|
21399
21580
|
}
|
|
21400
|
-
return { metadata: metadata2, body };
|
|
21401
21581
|
}
|
|
21402
21582
|
async function getSkillMetadata(skillName) {
|
|
21403
|
-
|
|
21404
|
-
|
|
21405
|
-
|
|
21406
|
-
|
|
21407
|
-
|
|
21408
|
-
|
|
21409
|
-
|
|
21410
|
-
|
|
21411
|
-
|
|
21412
|
-
|
|
21413
|
-
|
|
21414
|
-
|
|
21415
|
-
|
|
21416
|
-
|
|
21583
|
+
const cached2 = skillMetadataCache.get(skillName);
|
|
21584
|
+
if (cached2 && !isCacheExpired(cached2.timestamp)) {
|
|
21585
|
+
return cached2.data;
|
|
21586
|
+
}
|
|
21587
|
+
console.error(`[SKILL-LOADER] Skill 元数据缓存过期或不存在,重新加载: ${skillName}`);
|
|
21588
|
+
const content = await getSkillContent(skillName);
|
|
21589
|
+
const { metadata: metadata2 } = await parseFrontmatter(content);
|
|
21590
|
+
const metadataObj = {
|
|
21591
|
+
name: metadata2.name || skillName,
|
|
21592
|
+
description: metadata2.description || `${skillName} skill`,
|
|
21593
|
+
constraint: metadata2.constraint,
|
|
21594
|
+
version: metadata2.version,
|
|
21595
|
+
tools: metadata2.tools || [],
|
|
21596
|
+
...metadata2
|
|
21597
|
+
// 包含其他自定义字段
|
|
21598
|
+
};
|
|
21599
|
+
skillMetadataCache.set(skillName, { data: metadataObj, timestamp: Date.now() });
|
|
21600
|
+
return metadataObj;
|
|
21417
21601
|
}
|
|
21418
21602
|
async function getAllSkillsMetadata() {
|
|
21419
21603
|
const skills = await getAvailableSkills();
|
|
21420
21604
|
return Promise.all(skills.map((skillName) => getSkillMetadata(skillName)));
|
|
21421
21605
|
}
|
|
21606
|
+
let globalTempSkillDir = null;
|
|
21607
|
+
function setTempSkillDir(tempDir) {
|
|
21608
|
+
globalTempSkillDir = tempDir;
|
|
21609
|
+
}
|
|
21610
|
+
function getTempSkillDir() {
|
|
21611
|
+
return globalTempSkillDir;
|
|
21612
|
+
}
|
|
21613
|
+
function getSkillResourceFiles(skillName) {
|
|
21614
|
+
if (!globalTempSkillDir) {
|
|
21615
|
+
return [];
|
|
21616
|
+
}
|
|
21617
|
+
const skillDir = resolve$3(globalTempSkillDir, skillName);
|
|
21618
|
+
if (!existsSync(skillDir)) {
|
|
21619
|
+
return [];
|
|
21620
|
+
}
|
|
21621
|
+
try {
|
|
21622
|
+
return readdirSync(skillDir).filter((name) => name !== "SKILL.md");
|
|
21623
|
+
} catch {
|
|
21624
|
+
return [];
|
|
21625
|
+
}
|
|
21626
|
+
}
|
|
21627
|
+
function getWorkspaceSkillDir(skillName) {
|
|
21628
|
+
return resolve$3(globalTempSkillDir, skillName);
|
|
21629
|
+
}
|
|
21630
|
+
function copyDirectorySync(src, dest) {
|
|
21631
|
+
mkdirSync(dest, { recursive: true });
|
|
21632
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
21633
|
+
for (const entry of entries) {
|
|
21634
|
+
const srcPath = resolve$3(src, entry.name);
|
|
21635
|
+
const destPath = resolve$3(dest, entry.name);
|
|
21636
|
+
if (entry.isDirectory()) {
|
|
21637
|
+
copyDirectorySync(srcPath, destPath);
|
|
21638
|
+
} else {
|
|
21639
|
+
copyFileSync(srcPath, destPath);
|
|
21640
|
+
}
|
|
21641
|
+
}
|
|
21642
|
+
}
|
|
21643
|
+
async function downloadRemoteFile(url, targetPath) {
|
|
21644
|
+
try {
|
|
21645
|
+
const headers = {};
|
|
21646
|
+
const gitlabToken = process.env.GITLAB_TOKEN;
|
|
21647
|
+
if (gitlabToken) {
|
|
21648
|
+
headers["PRIVATE-TOKEN"] = gitlabToken;
|
|
21649
|
+
}
|
|
21650
|
+
const response = await fetch(url, { headers });
|
|
21651
|
+
if (!response.ok) {
|
|
21652
|
+
console.error(`[SKILL-LOADER] 下载远程文件失败: ${url} - ${response.status}`);
|
|
21653
|
+
return false;
|
|
21654
|
+
}
|
|
21655
|
+
const content = await response.text();
|
|
21656
|
+
if (content.trim().startsWith("<!DOCTYPE") || content.trim().startsWith("<html")) {
|
|
21657
|
+
console.error(`[SKILL-LOADER] 下载远程文件返回了 HTML 页面: ${url}`);
|
|
21658
|
+
return false;
|
|
21659
|
+
}
|
|
21660
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
21661
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
21662
|
+
return true;
|
|
21663
|
+
} catch (error2) {
|
|
21664
|
+
console.error(`[SKILL-LOADER] 下载远程文件异常: ${url}`, error2);
|
|
21665
|
+
return false;
|
|
21666
|
+
}
|
|
21667
|
+
}
|
|
21668
|
+
async function listRemoteSkillFiles(skillName, source) {
|
|
21669
|
+
try {
|
|
21670
|
+
const parsed = await parseGitLabUrl(source.originalUrl);
|
|
21671
|
+
if (!parsed) {
|
|
21672
|
+
console.error(`[SKILL-LOADER] 无法解析远程 URL: ${source.originalUrl}`);
|
|
21673
|
+
return [];
|
|
21674
|
+
}
|
|
21675
|
+
const { host, projectPath, ref: ref2, path: basePath } = parsed;
|
|
21676
|
+
const skillPath = basePath ? `${basePath}/${skillName}` : skillName;
|
|
21677
|
+
const encodedProjectPath = encodeURIComponent(projectPath);
|
|
21678
|
+
const apiUrl = `${host}/api/v4/projects/${encodedProjectPath}/repository/tree?ref=${encodeURIComponent(ref2)}&path=${encodeURIComponent(skillPath)}`;
|
|
21679
|
+
const headers = {};
|
|
21680
|
+
const gitlabToken = process.env.GITLAB_TOKEN;
|
|
21681
|
+
if (gitlabToken) {
|
|
21682
|
+
headers["PRIVATE-TOKEN"] = gitlabToken;
|
|
21683
|
+
}
|
|
21684
|
+
const response = await fetch(apiUrl, { headers });
|
|
21685
|
+
if (!response.ok) {
|
|
21686
|
+
console.error(`[SKILL-LOADER] 获取远程文件列表失败: ${response.status}`);
|
|
21687
|
+
return [];
|
|
21688
|
+
}
|
|
21689
|
+
const items2 = await response.json();
|
|
21690
|
+
return items2.filter((item) => item.type === "blob" && item.name !== "SKILL.md").map((item) => item.name);
|
|
21691
|
+
} catch (error2) {
|
|
21692
|
+
console.error(`[SKILL-LOADER] 获取远程文件列表异常:`, error2);
|
|
21693
|
+
return [];
|
|
21694
|
+
}
|
|
21695
|
+
}
|
|
21696
|
+
async function extractRemoteSkillResourcesToTempDir(skillName, source) {
|
|
21697
|
+
const extractedPaths = [];
|
|
21698
|
+
const files = await listRemoteSkillFiles(skillName, source);
|
|
21699
|
+
if (files.length === 0) {
|
|
21700
|
+
console.error(`[SKILL-LOADER] 远程 Skill ${skillName} 没有资源文件`);
|
|
21701
|
+
return [];
|
|
21702
|
+
}
|
|
21703
|
+
console.error(`[SKILL-LOADER] 发现远程 Skill ${skillName} 的资源文件: ${files.join(", ")}`);
|
|
21704
|
+
for (const fileName of files) {
|
|
21705
|
+
const fileUrl = `${source.baseUrl}/${skillName}/${fileName}`;
|
|
21706
|
+
const targetDir = getWorkspaceSkillDir(skillName);
|
|
21707
|
+
const targetPath = resolve$3(targetDir, fileName);
|
|
21708
|
+
const success = await downloadRemoteFile(fileUrl, targetPath);
|
|
21709
|
+
if (success) {
|
|
21710
|
+
extractedPaths.push(targetPath);
|
|
21711
|
+
console.error(`[SKILL-LOADER] 已下载远程资源: ${skillName}/${fileName} -> ${targetPath}`);
|
|
21712
|
+
} else {
|
|
21713
|
+
console.error(`[SKILL-LOADER] 下载远程资源失败: ${skillName}/${fileName}`);
|
|
21714
|
+
}
|
|
21715
|
+
}
|
|
21716
|
+
return extractedPaths;
|
|
21717
|
+
}
|
|
21718
|
+
async function extractSkillResourcesToTempDir(skillName) {
|
|
21719
|
+
const config2 = getSkillsConfig();
|
|
21720
|
+
if (config2.source === "remote") {
|
|
21721
|
+
const source = skillSourceMap.get(skillName);
|
|
21722
|
+
if (source) {
|
|
21723
|
+
console.error(`[SKILL-LOADER] 从远程源 ${source.name} 提取 Skill ${skillName} 的资源`);
|
|
21724
|
+
return extractRemoteSkillResourcesToTempDir(skillName, source);
|
|
21725
|
+
}
|
|
21726
|
+
if (config2.sources && config2.sources.length > 0) {
|
|
21727
|
+
console.error(`[SKILL-LOADER] 使用默认远程源提取 Skill ${skillName} 的资源`);
|
|
21728
|
+
return extractRemoteSkillResourcesToTempDir(skillName, config2.sources[0]);
|
|
21729
|
+
}
|
|
21730
|
+
console.error(`[SKILL-LOADER] 远程模式下找不到 Skill ${skillName} 的源配置`);
|
|
21731
|
+
return [];
|
|
21732
|
+
}
|
|
21733
|
+
const skillDir = getSkillsDir();
|
|
21734
|
+
const sourceDir = resolve$3(skillDir, skillName);
|
|
21735
|
+
if (!existsSync(sourceDir)) {
|
|
21736
|
+
console.error(`[SKILL-LOADER] Skill 目录不存在: ${sourceDir}`);
|
|
21737
|
+
return [];
|
|
21738
|
+
}
|
|
21739
|
+
const extractedPaths = [];
|
|
21740
|
+
try {
|
|
21741
|
+
const entries = readdirSync(sourceDir, { withFileTypes: true });
|
|
21742
|
+
for (const entry of entries) {
|
|
21743
|
+
if (entry.name === "SKILL.md") {
|
|
21744
|
+
continue;
|
|
21745
|
+
}
|
|
21746
|
+
const sourcePath = resolve$3(sourceDir, entry.name);
|
|
21747
|
+
const targetDir = getWorkspaceSkillDir(skillName);
|
|
21748
|
+
const targetPath = resolve$3(targetDir, entry.name);
|
|
21749
|
+
try {
|
|
21750
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
21751
|
+
if (entry.isDirectory()) {
|
|
21752
|
+
copyDirectorySync(sourcePath, targetPath);
|
|
21753
|
+
} else {
|
|
21754
|
+
copyFileSync(sourcePath, targetPath);
|
|
21755
|
+
}
|
|
21756
|
+
extractedPaths.push(targetPath);
|
|
21757
|
+
console.error(`[SKILL-LOADER] 已提取资源: ${skillName}/${entry.name} -> ${targetPath}`);
|
|
21758
|
+
} catch (error2) {
|
|
21759
|
+
console.error(`[SKILL-LOADER] 提取资源失败: ${skillName}/${entry.name}`, error2);
|
|
21760
|
+
}
|
|
21761
|
+
}
|
|
21762
|
+
} catch (error2) {
|
|
21763
|
+
console.error(`[SKILL-LOADER] 读取 Skill 目录失败: ${sourceDir}`, error2);
|
|
21764
|
+
}
|
|
21765
|
+
return extractedPaths;
|
|
21766
|
+
}
|
|
21767
|
+
async function extractAllSkillResourcesToTempDir(tempDir) {
|
|
21768
|
+
setTempSkillDir(tempDir);
|
|
21769
|
+
const skills = await getAvailableSkills();
|
|
21770
|
+
const allExtracted = {};
|
|
21771
|
+
console.error(`[SKILL-LOADER] 开始提取 ${skills.length} 个 Skill 的资源到临时目录: ${tempDir}`);
|
|
21772
|
+
for (const skillName of skills) {
|
|
21773
|
+
const paths = await extractSkillResourcesToTempDir(skillName);
|
|
21774
|
+
if (paths.length > 0) {
|
|
21775
|
+
allExtracted[skillName] = paths;
|
|
21776
|
+
}
|
|
21777
|
+
}
|
|
21778
|
+
const totalCount = Object.values(allExtracted).flat().length;
|
|
21779
|
+
console.error(`[SKILL-LOADER] 资源提取完成,共 ${totalCount} 个文件`);
|
|
21780
|
+
return allExtracted;
|
|
21781
|
+
}
|
|
21782
|
+
const FIXED_SKILL_DIR = join(tmpdir(), "mcp-pipe-skills");
|
|
21783
|
+
let skillResourcesDir = FIXED_SKILL_DIR;
|
|
21422
21784
|
async function createServer() {
|
|
21423
21785
|
const server = new McpServer({
|
|
21424
21786
|
name: "mcp-pipe",
|
|
@@ -21440,6 +21802,27 @@ async function createServer() {
|
|
|
21440
21802
|
const skillLoaded = skills.includes(metadata2.name);
|
|
21441
21803
|
const skillMetadata = await getSkillMetadata(metadata2.name);
|
|
21442
21804
|
const skillContent = await getSkillContent(metadata2.name);
|
|
21805
|
+
let injectedContent = skillContent;
|
|
21806
|
+
const currentTempDir = getTempSkillDir();
|
|
21807
|
+
if (currentTempDir) {
|
|
21808
|
+
const skillResourceDir = join(currentTempDir, metadata2.name);
|
|
21809
|
+
const resourceFiles = getSkillResourceFiles(metadata2.name);
|
|
21810
|
+
let resourceNotice = `> 📁 **本 Skill 的资源文件**:
|
|
21811
|
+
`;
|
|
21812
|
+
if (resourceFiles.length > 0) {
|
|
21813
|
+
for (const file of resourceFiles) {
|
|
21814
|
+
resourceNotice += `> - \`${join(skillResourceDir, file)}\`
|
|
21815
|
+
`;
|
|
21816
|
+
}
|
|
21817
|
+
} else {
|
|
21818
|
+
resourceNotice += "> (无额外资源文件)\n";
|
|
21819
|
+
}
|
|
21820
|
+
resourceNotice += `>
|
|
21821
|
+
> 💡 **提示**: 当 Skill 文档中提到读取配置文件或模板文件时,请使用上述完整路径
|
|
21822
|
+
|
|
21823
|
+
`;
|
|
21824
|
+
injectedContent = resourceNotice + skillContent;
|
|
21825
|
+
}
|
|
21443
21826
|
return {
|
|
21444
21827
|
content: [{
|
|
21445
21828
|
type: "text",
|
|
@@ -21447,7 +21830,7 @@ async function createServer() {
|
|
|
21447
21830
|
|
|
21448
21831
|
📖 ${skillMetadata.description}
|
|
21449
21832
|
|
|
21450
|
-
${
|
|
21833
|
+
${injectedContent}
|
|
21451
21834
|
|
|
21452
21835
|
---
|
|
21453
21836
|
${skillLoaded ? "✅" : "❌"} Skills 加载状态: ${skillLoaded ? `${metadata2.name} skill 已加载` : `${metadata2.name} skill 未加载`}
|
|
@@ -21511,8 +21894,34 @@ async function preloadSkills() {
|
|
|
21511
21894
|
}
|
|
21512
21895
|
}
|
|
21513
21896
|
}
|
|
21897
|
+
function cleanupSkillDir() {
|
|
21898
|
+
if (existsSync(skillResourcesDir)) {
|
|
21899
|
+
try {
|
|
21900
|
+
rmSync(skillResourcesDir, { recursive: true, force: true });
|
|
21901
|
+
console.error(`[MCP-PIPE] Skill 资源目录已清理: ${skillResourcesDir}`);
|
|
21902
|
+
} catch (error2) {
|
|
21903
|
+
console.error(`[MCP-PIPE] 清理 Skill 资源目录失败: ${skillResourcesDir}`, error2);
|
|
21904
|
+
}
|
|
21905
|
+
}
|
|
21906
|
+
}
|
|
21514
21907
|
async function main() {
|
|
21908
|
+
process.on("exit", cleanupSkillDir);
|
|
21909
|
+
process.on("SIGINT", () => {
|
|
21910
|
+
console.error("[MCP-PIPE] 收到 SIGINT,正在清理...");
|
|
21911
|
+
cleanupSkillDir();
|
|
21912
|
+
process.exit(0);
|
|
21913
|
+
});
|
|
21914
|
+
process.on("SIGTERM", () => {
|
|
21915
|
+
console.error("[MCP-PIPE] 收到 SIGTERM,正在清理...");
|
|
21916
|
+
cleanupSkillDir();
|
|
21917
|
+
process.exit(0);
|
|
21918
|
+
});
|
|
21919
|
+
if (!existsSync(skillResourcesDir)) {
|
|
21920
|
+
mkdirSync(skillResourcesDir, { recursive: true });
|
|
21921
|
+
}
|
|
21922
|
+
console.error(`[MCP-PIPE] Skill 资源目录: ${skillResourcesDir}`);
|
|
21515
21923
|
await preloadSkills();
|
|
21924
|
+
await extractAllSkillResourcesToTempDir(skillResourcesDir);
|
|
21516
21925
|
initMCPClients();
|
|
21517
21926
|
const server = await createServer();
|
|
21518
21927
|
const transport = new StdioServerTransport();
|
|
@@ -21521,5 +21930,6 @@ async function main() {
|
|
|
21521
21930
|
}
|
|
21522
21931
|
main().catch((error2) => {
|
|
21523
21932
|
console.error("[MCP-PIPE] 致命错误:", error2);
|
|
21933
|
+
cleanupSkillDir();
|
|
21524
21934
|
process.exit(1);
|
|
21525
21935
|
});
|