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 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: `${prefix}${tool.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: `${prefix}${originalToolName}`,
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: `${prefix}${tool.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, args);
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, args);
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 skillsUrl = process.env.SKILLS_URL;
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 rawUrl = skillsUrl.replace("/tree/", "/raw/").replace(/\/$/, "");
21314
+ const source = parseSkillSource(skillsUrl, 0);
21204
21315
  return {
21205
21316
  source: "remote",
21206
- baseUrl: rawUrl,
21207
- originalUrl: skillsUrl
21208
- // 保留原始 URL 用于解析
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 skills2 = skillsListEnv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
21293
- remoteSkillsList = skills2;
21294
- console.error(`[SKILL-LOADER] 从环境变量加载 skills: ${skills2.join(", ")}`);
21295
- return skills2;
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] 未配置 SKILLS_LIST,自动遍历远程目录...`);
21298
- const skills = await fetchRemoteSkillsFromDirectory(config2);
21299
- remoteSkillsList = skills;
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" && config2.baseUrl) {
21361
- return loadRemoteSkillContent(skillName, config2.baseUrl);
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
- if (!skillCache.has(skillName)) {
21374
- const content = await loadSkillContent(skillName);
21375
- skillCache.set(skillName, content);
21544
+ const cached2 = skillCache.get(skillName);
21545
+ if (cached2 && !isCacheExpired(cached2.timestamp)) {
21546
+ return cached2.data;
21376
21547
  }
21377
- return skillCache.get(skillName);
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
- const metadata2 = {};
21388
- const lines = frontmatterText.split("\n");
21389
- for (const line of lines) {
21390
- const colonIndex = line.indexOf(":");
21391
- if (colonIndex > 0) {
21392
- const key = line.slice(0, colonIndex).trim();
21393
- let value = line.slice(colonIndex + 1).trim();
21394
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
21395
- value = value.slice(1, -1);
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
- if (!skillMetadataCache.has(skillName)) {
21404
- const content = await getSkillContent(skillName);
21405
- const { metadata: metadata2 } = parseFrontmatter(content);
21406
- const metadataObj = {
21407
- name: metadata2.name || skillName,
21408
- description: metadata2.description || `${skillName} skill`,
21409
- constraint: metadata2.constraint,
21410
- version: metadata2.version,
21411
- ...metadata2
21412
- // 包含其他自定义字段
21413
- };
21414
- skillMetadataCache.set(skillName, metadataObj);
21415
- }
21416
- return skillMetadataCache.get(skillName);
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
- ${skillContent}
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
  });