postar-pipe-mcp 0.0.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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";
@@ -21197,15 +21198,75 @@ async function handleProxiedTool(name, args) {
21197
21198
  const skillCache = /* @__PURE__ */ new Map();
21198
21199
  const skillMetadataCache = /* @__PURE__ */ new Map();
21199
21200
  let remoteSkillsList = null;
21201
+ function parseSkillSource(url, index) {
21202
+ const rawUrl = url.replace("/tree/", "/raw/").replace(/\/$/, "");
21203
+ const match = url.match(/\/([^\/]+)\/[^\/]+\/tree\//);
21204
+ const name = match ? match[1] : `source-${index}`;
21205
+ return {
21206
+ name,
21207
+ baseUrl: rawUrl,
21208
+ originalUrl: url
21209
+ };
21210
+ }
21211
+ function parseCommandLineArgs() {
21212
+ const result = {};
21213
+ const skillUrls = [];
21214
+ for (let i = 0; i < process.argv.length; i++) {
21215
+ const arg = process.argv[i];
21216
+ if (arg === "--skill-url" && i + 1 < process.argv.length) {
21217
+ skillUrls.push(process.argv[i + 1]);
21218
+ } else if (arg.startsWith("--skill-url=")) {
21219
+ skillUrls.push(arg.split("=")[1]);
21220
+ }
21221
+ if (arg.startsWith("--skills-urls=")) {
21222
+ result.skillsUrls = arg.split("=")[1];
21223
+ } else if (arg === "--skills-urls" && i + 1 < process.argv.length) {
21224
+ result.skillsUrls = process.argv[i + 1];
21225
+ }
21226
+ if (arg.startsWith("--skills-url=")) {
21227
+ result.skillsUrl = arg.split("=")[1];
21228
+ } else if (arg === "--skills-url" && i + 1 < process.argv.length) {
21229
+ result.skillsUrl = process.argv[i + 1];
21230
+ }
21231
+ }
21232
+ if (skillUrls.length > 0) {
21233
+ result.skillUrls = skillUrls;
21234
+ }
21235
+ return result;
21236
+ }
21200
21237
  function getSkillsConfig() {
21201
- const skillsUrl = process.env.SKILLS_URL;
21238
+ const cliArgs = parseCommandLineArgs();
21239
+ if (cliArgs.skillUrls && cliArgs.skillUrls.length > 0) {
21240
+ const sources = cliArgs.skillUrls.map((url, index) => parseSkillSource(url.trim(), index)).filter((source) => source.originalUrl.length > 0);
21241
+ if (sources.length > 0) {
21242
+ return {
21243
+ source: "remote",
21244
+ sources,
21245
+ baseUrl: sources[0].baseUrl,
21246
+ originalUrl: sources[0].originalUrl
21247
+ };
21248
+ }
21249
+ }
21250
+ const skillsUrls = cliArgs.skillsUrls || process.env.SKILLS_URLS;
21251
+ if (skillsUrls) {
21252
+ const sources = skillsUrls.split(",").map((url, index) => parseSkillSource(url.trim(), index)).filter((source) => source.originalUrl.length > 0);
21253
+ if (sources.length > 0) {
21254
+ return {
21255
+ source: "remote",
21256
+ sources,
21257
+ baseUrl: sources[0].baseUrl,
21258
+ originalUrl: sources[0].originalUrl
21259
+ };
21260
+ }
21261
+ }
21262
+ const skillsUrl = cliArgs.skillsUrl || process.env.SKILLS_URL;
21202
21263
  if (skillsUrl) {
21203
- const rawUrl = skillsUrl.replace("/tree/", "/raw/").replace(/\/$/, "");
21264
+ const source = parseSkillSource(skillsUrl, 0);
21204
21265
  return {
21205
21266
  source: "remote",
21206
- baseUrl: rawUrl,
21207
- originalUrl: skillsUrl
21208
- // 保留原始 URL 用于解析
21267
+ sources: [source],
21268
+ baseUrl: source.baseUrl,
21269
+ originalUrl: source.originalUrl
21209
21270
  };
21210
21271
  }
21211
21272
  return {
@@ -21283,21 +21344,52 @@ async function fetchRemoteSkillsFromDirectory(config2) {
21283
21344
  return [];
21284
21345
  }
21285
21346
  }
21347
+ const skillSourceMap = /* @__PURE__ */ new Map();
21286
21348
  async function fetchRemoteSkillsList(config2) {
21287
21349
  if (remoteSkillsList) {
21288
21350
  return remoteSkillsList;
21289
21351
  }
21290
21352
  const skillsListEnv = process.env.SKILLS_LIST;
21291
21353
  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;
21354
+ const skills = skillsListEnv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
21355
+ remoteSkillsList = skills;
21356
+ console.error(`[SKILL-LOADER] 从环境变量加载 skills: ${skills.join(", ")}`);
21357
+ return skills;
21358
+ }
21359
+ console.error(`[SKILL-LOADER] 未配置 SKILLS_LIST,自动遍历所有远程目录...`);
21360
+ const allSkills = [];
21361
+ skillSourceMap.clear();
21362
+ if (config2.sources && config2.sources.length > 0) {
21363
+ for (const source of config2.sources) {
21364
+ console.error(`[SKILL-LOADER] 扫描源: ${source.name} (${source.originalUrl})`);
21365
+ const sourceConfig = {
21366
+ source: "remote",
21367
+ baseUrl: source.baseUrl,
21368
+ originalUrl: source.originalUrl
21369
+ };
21370
+ const skills = await fetchRemoteSkillsFromDirectory(sourceConfig);
21371
+ console.error(`[SKILL-LOADER] 源 ${source.name} 发现 skills: ${skills.join(", ")}`);
21372
+ for (const skillName of skills) {
21373
+ if (!allSkills.includes(skillName)) {
21374
+ allSkills.push(skillName);
21375
+ }
21376
+ skillSourceMap.set(skillName, source);
21377
+ }
21378
+ }
21379
+ } else if (config2.baseUrl && config2.originalUrl) {
21380
+ const skills = await fetchRemoteSkillsFromDirectory(config2);
21381
+ for (const skillName of skills) {
21382
+ allSkills.push(skillName);
21383
+ skillSourceMap.set(skillName, {
21384
+ name: "default",
21385
+ baseUrl: config2.baseUrl,
21386
+ originalUrl: config2.originalUrl
21387
+ });
21388
+ }
21296
21389
  }
21297
- console.error(`[SKILL-LOADER] 未配置 SKILLS_LIST,自动遍历远程目录...`);
21298
- const skills = await fetchRemoteSkillsFromDirectory(config2);
21299
- remoteSkillsList = skills;
21300
- return skills;
21390
+ console.error(`[SKILL-LOADER] 汇总所有 skills: ${allSkills.join(", ")}`);
21391
+ remoteSkillsList = allSkills;
21392
+ return allSkills;
21301
21393
  }
21302
21394
  async function loadRemoteSkillContent(skillName, baseUrl) {
21303
21395
  const skillUrl = `${baseUrl}/${skillName}/SKILL.md`;
@@ -21357,8 +21449,20 @@ async function getAvailableSkills() {
21357
21449
  }
21358
21450
  async function loadSkillContent(skillName) {
21359
21451
  const config2 = getSkillsConfig();
21360
- if (config2.source === "remote" && config2.baseUrl) {
21361
- return loadRemoteSkillContent(skillName, config2.baseUrl);
21452
+ if (config2.source === "remote") {
21453
+ const source = skillSourceMap.get(skillName);
21454
+ if (source) {
21455
+ console.error(`[SKILL-LOADER] 从源 ${source.name} 加载 skill: ${skillName}`);
21456
+ return loadRemoteSkillContent(skillName, source.baseUrl);
21457
+ }
21458
+ if (config2.sources && config2.sources.length > 0) {
21459
+ const firstSource = config2.sources[0];
21460
+ console.error(`[SKILL-LOADER] 未找到来源映射,使用默认源 ${firstSource.name} 加载: ${skillName}`);
21461
+ return loadRemoteSkillContent(skillName, firstSource.baseUrl);
21462
+ }
21463
+ if (config2.baseUrl) {
21464
+ return loadRemoteSkillContent(skillName, config2.baseUrl);
21465
+ }
21362
21466
  }
21363
21467
  const skillPath = resolve$3(getSkillsDir(), skillName, "SKILL.md");
21364
21468
  try {
@@ -21376,7 +21480,7 @@ async function getSkillContent(skillName) {
21376
21480
  }
21377
21481
  return skillCache.get(skillName);
21378
21482
  }
21379
- function parseFrontmatter(content) {
21483
+ async function parseFrontmatter(content) {
21380
21484
  const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
21381
21485
  const match = content.match(frontmatterRegex);
21382
21486
  if (!match) {
@@ -21384,30 +21488,37 @@ function parseFrontmatter(content) {
21384
21488
  }
21385
21489
  const frontmatterText = match[1];
21386
21490
  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);
21491
+ try {
21492
+ const yaml = await import("js-yaml");
21493
+ const metadata2 = yaml.load(frontmatterText) || {};
21494
+ return { metadata: metadata2, body };
21495
+ } catch {
21496
+ const metadata2 = {};
21497
+ const lines = frontmatterText.split("\n");
21498
+ for (const line of lines) {
21499
+ const colonIndex = line.indexOf(":");
21500
+ if (colonIndex > 0 && !line.startsWith("-") && !line.startsWith(" ")) {
21501
+ const key = line.slice(0, colonIndex).trim();
21502
+ let value = line.slice(colonIndex + 1).trim();
21503
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
21504
+ value = value.slice(1, -1);
21505
+ }
21506
+ metadata2[key] = value;
21396
21507
  }
21397
- metadata2[key] = value;
21398
21508
  }
21509
+ return { metadata: metadata2, body };
21399
21510
  }
21400
- return { metadata: metadata2, body };
21401
21511
  }
21402
21512
  async function getSkillMetadata(skillName) {
21403
21513
  if (!skillMetadataCache.has(skillName)) {
21404
21514
  const content = await getSkillContent(skillName);
21405
- const { metadata: metadata2 } = parseFrontmatter(content);
21515
+ const { metadata: metadata2 } = await parseFrontmatter(content);
21406
21516
  const metadataObj = {
21407
21517
  name: metadata2.name || skillName,
21408
21518
  description: metadata2.description || `${skillName} skill`,
21409
21519
  constraint: metadata2.constraint,
21410
21520
  version: metadata2.version,
21521
+ tools: metadata2.tools || [],
21411
21522
  ...metadata2
21412
21523
  // 包含其他自定义字段
21413
21524
  };
@@ -21419,6 +21530,184 @@ async function getAllSkillsMetadata() {
21419
21530
  const skills = await getAvailableSkills();
21420
21531
  return Promise.all(skills.map((skillName) => getSkillMetadata(skillName)));
21421
21532
  }
21533
+ let globalTempSkillDir = null;
21534
+ function setTempSkillDir(tempDir) {
21535
+ globalTempSkillDir = tempDir;
21536
+ }
21537
+ function getTempSkillDir() {
21538
+ return globalTempSkillDir;
21539
+ }
21540
+ function getSkillResourceFiles(skillName) {
21541
+ if (!globalTempSkillDir) {
21542
+ return [];
21543
+ }
21544
+ const skillDir = resolve$3(globalTempSkillDir, skillName);
21545
+ if (!existsSync(skillDir)) {
21546
+ return [];
21547
+ }
21548
+ try {
21549
+ return readdirSync(skillDir).filter((name) => name !== "SKILL.md");
21550
+ } catch {
21551
+ return [];
21552
+ }
21553
+ }
21554
+ function getWorkspaceSkillDir(skillName) {
21555
+ return resolve$3(globalTempSkillDir, skillName);
21556
+ }
21557
+ function copyDirectorySync(src, dest) {
21558
+ mkdirSync(dest, { recursive: true });
21559
+ const entries = readdirSync(src, { withFileTypes: true });
21560
+ for (const entry of entries) {
21561
+ const srcPath = resolve$3(src, entry.name);
21562
+ const destPath = resolve$3(dest, entry.name);
21563
+ if (entry.isDirectory()) {
21564
+ copyDirectorySync(srcPath, destPath);
21565
+ } else {
21566
+ copyFileSync(srcPath, destPath);
21567
+ }
21568
+ }
21569
+ }
21570
+ async function downloadRemoteFile(url, targetPath) {
21571
+ try {
21572
+ const headers = {};
21573
+ const gitlabToken = process.env.GITLAB_TOKEN;
21574
+ if (gitlabToken) {
21575
+ headers["PRIVATE-TOKEN"] = gitlabToken;
21576
+ }
21577
+ const response = await fetch(url, { headers });
21578
+ if (!response.ok) {
21579
+ console.error(`[SKILL-LOADER] 下载远程文件失败: ${url} - ${response.status}`);
21580
+ return false;
21581
+ }
21582
+ const content = await response.text();
21583
+ if (content.trim().startsWith("<!DOCTYPE") || content.trim().startsWith("<html")) {
21584
+ console.error(`[SKILL-LOADER] 下载远程文件返回了 HTML 页面: ${url}`);
21585
+ return false;
21586
+ }
21587
+ mkdirSync(dirname(targetPath), { recursive: true });
21588
+ writeFileSync(targetPath, content, "utf-8");
21589
+ return true;
21590
+ } catch (error2) {
21591
+ console.error(`[SKILL-LOADER] 下载远程文件异常: ${url}`, error2);
21592
+ return false;
21593
+ }
21594
+ }
21595
+ async function listRemoteSkillFiles(skillName, source) {
21596
+ try {
21597
+ const parsed = await parseGitLabUrl(source.originalUrl);
21598
+ if (!parsed) {
21599
+ console.error(`[SKILL-LOADER] 无法解析远程 URL: ${source.originalUrl}`);
21600
+ return [];
21601
+ }
21602
+ const { host, projectPath, ref: ref2, path: basePath } = parsed;
21603
+ const skillPath = basePath ? `${basePath}/${skillName}` : skillName;
21604
+ const encodedProjectPath = encodeURIComponent(projectPath);
21605
+ const apiUrl = `${host}/api/v4/projects/${encodedProjectPath}/repository/tree?ref=${encodeURIComponent(ref2)}&path=${encodeURIComponent(skillPath)}`;
21606
+ const headers = {};
21607
+ const gitlabToken = process.env.GITLAB_TOKEN;
21608
+ if (gitlabToken) {
21609
+ headers["PRIVATE-TOKEN"] = gitlabToken;
21610
+ }
21611
+ const response = await fetch(apiUrl, { headers });
21612
+ if (!response.ok) {
21613
+ console.error(`[SKILL-LOADER] 获取远程文件列表失败: ${response.status}`);
21614
+ return [];
21615
+ }
21616
+ const items2 = await response.json();
21617
+ return items2.filter((item) => item.type === "blob" && item.name !== "SKILL.md").map((item) => item.name);
21618
+ } catch (error2) {
21619
+ console.error(`[SKILL-LOADER] 获取远程文件列表异常:`, error2);
21620
+ return [];
21621
+ }
21622
+ }
21623
+ async function extractRemoteSkillResourcesToTempDir(skillName, source) {
21624
+ const extractedPaths = [];
21625
+ const files = await listRemoteSkillFiles(skillName, source);
21626
+ if (files.length === 0) {
21627
+ console.error(`[SKILL-LOADER] 远程 Skill ${skillName} 没有资源文件`);
21628
+ return [];
21629
+ }
21630
+ console.error(`[SKILL-LOADER] 发现远程 Skill ${skillName} 的资源文件: ${files.join(", ")}`);
21631
+ for (const fileName of files) {
21632
+ const fileUrl = `${source.baseUrl}/${skillName}/${fileName}`;
21633
+ const targetDir = getWorkspaceSkillDir(skillName);
21634
+ const targetPath = resolve$3(targetDir, fileName);
21635
+ const success = await downloadRemoteFile(fileUrl, targetPath);
21636
+ if (success) {
21637
+ extractedPaths.push(targetPath);
21638
+ console.error(`[SKILL-LOADER] 已下载远程资源: ${skillName}/${fileName} -> ${targetPath}`);
21639
+ } else {
21640
+ console.error(`[SKILL-LOADER] 下载远程资源失败: ${skillName}/${fileName}`);
21641
+ }
21642
+ }
21643
+ return extractedPaths;
21644
+ }
21645
+ async function extractSkillResourcesToTempDir(skillName) {
21646
+ const config2 = getSkillsConfig();
21647
+ if (config2.source === "remote") {
21648
+ const source = skillSourceMap.get(skillName);
21649
+ if (source) {
21650
+ console.error(`[SKILL-LOADER] 从远程源 ${source.name} 提取 Skill ${skillName} 的资源`);
21651
+ return extractRemoteSkillResourcesToTempDir(skillName, source);
21652
+ }
21653
+ if (config2.sources && config2.sources.length > 0) {
21654
+ console.error(`[SKILL-LOADER] 使用默认远程源提取 Skill ${skillName} 的资源`);
21655
+ return extractRemoteSkillResourcesToTempDir(skillName, config2.sources[0]);
21656
+ }
21657
+ console.error(`[SKILL-LOADER] 远程模式下找不到 Skill ${skillName} 的源配置`);
21658
+ return [];
21659
+ }
21660
+ const skillDir = getSkillsDir();
21661
+ const sourceDir = resolve$3(skillDir, skillName);
21662
+ if (!existsSync(sourceDir)) {
21663
+ console.error(`[SKILL-LOADER] Skill 目录不存在: ${sourceDir}`);
21664
+ return [];
21665
+ }
21666
+ const extractedPaths = [];
21667
+ try {
21668
+ const entries = readdirSync(sourceDir, { withFileTypes: true });
21669
+ for (const entry of entries) {
21670
+ if (entry.name === "SKILL.md") {
21671
+ continue;
21672
+ }
21673
+ const sourcePath = resolve$3(sourceDir, entry.name);
21674
+ const targetDir = getWorkspaceSkillDir(skillName);
21675
+ const targetPath = resolve$3(targetDir, entry.name);
21676
+ try {
21677
+ mkdirSync(dirname(targetPath), { recursive: true });
21678
+ if (entry.isDirectory()) {
21679
+ copyDirectorySync(sourcePath, targetPath);
21680
+ } else {
21681
+ copyFileSync(sourcePath, targetPath);
21682
+ }
21683
+ extractedPaths.push(targetPath);
21684
+ console.error(`[SKILL-LOADER] 已提取资源: ${skillName}/${entry.name} -> ${targetPath}`);
21685
+ } catch (error2) {
21686
+ console.error(`[SKILL-LOADER] 提取资源失败: ${skillName}/${entry.name}`, error2);
21687
+ }
21688
+ }
21689
+ } catch (error2) {
21690
+ console.error(`[SKILL-LOADER] 读取 Skill 目录失败: ${sourceDir}`, error2);
21691
+ }
21692
+ return extractedPaths;
21693
+ }
21694
+ async function extractAllSkillResourcesToTempDir(tempDir) {
21695
+ setTempSkillDir(tempDir);
21696
+ const skills = await getAvailableSkills();
21697
+ const allExtracted = {};
21698
+ console.error(`[SKILL-LOADER] 开始提取 ${skills.length} 个 Skill 的资源到临时目录: ${tempDir}`);
21699
+ for (const skillName of skills) {
21700
+ const paths = await extractSkillResourcesToTempDir(skillName);
21701
+ if (paths.length > 0) {
21702
+ allExtracted[skillName] = paths;
21703
+ }
21704
+ }
21705
+ const totalCount = Object.values(allExtracted).flat().length;
21706
+ console.error(`[SKILL-LOADER] 资源提取完成,共 ${totalCount} 个文件`);
21707
+ return allExtracted;
21708
+ }
21709
+ const FIXED_SKILL_DIR = join(tmpdir(), "mcp-pipe-skills");
21710
+ let skillResourcesDir = FIXED_SKILL_DIR;
21422
21711
  async function createServer() {
21423
21712
  const server = new McpServer({
21424
21713
  name: "mcp-pipe",
@@ -21440,6 +21729,27 @@ async function createServer() {
21440
21729
  const skillLoaded = skills.includes(metadata2.name);
21441
21730
  const skillMetadata = await getSkillMetadata(metadata2.name);
21442
21731
  const skillContent = await getSkillContent(metadata2.name);
21732
+ let injectedContent = skillContent;
21733
+ const currentTempDir = getTempSkillDir();
21734
+ if (currentTempDir) {
21735
+ const skillResourceDir = join(currentTempDir, metadata2.name);
21736
+ const resourceFiles = getSkillResourceFiles(metadata2.name);
21737
+ let resourceNotice = `> 📁 **本 Skill 的资源文件**:
21738
+ `;
21739
+ if (resourceFiles.length > 0) {
21740
+ for (const file of resourceFiles) {
21741
+ resourceNotice += `> - \`${join(skillResourceDir, file)}\`
21742
+ `;
21743
+ }
21744
+ } else {
21745
+ resourceNotice += "> (无额外资源文件)\n";
21746
+ }
21747
+ resourceNotice += `>
21748
+ > 💡 **提示**: 当 Skill 文档中提到读取配置文件或模板文件时,请使用上述完整路径
21749
+
21750
+ `;
21751
+ injectedContent = resourceNotice + skillContent;
21752
+ }
21443
21753
  return {
21444
21754
  content: [{
21445
21755
  type: "text",
@@ -21447,7 +21757,7 @@ async function createServer() {
21447
21757
 
21448
21758
  📖 ${skillMetadata.description}
21449
21759
 
21450
- ${skillContent}
21760
+ ${injectedContent}
21451
21761
 
21452
21762
  ---
21453
21763
  ${skillLoaded ? "✅" : "❌"} Skills 加载状态: ${skillLoaded ? `${metadata2.name} skill 已加载` : `${metadata2.name} skill 未加载`}
@@ -21511,8 +21821,34 @@ async function preloadSkills() {
21511
21821
  }
21512
21822
  }
21513
21823
  }
21824
+ function cleanupSkillDir() {
21825
+ if (existsSync(skillResourcesDir)) {
21826
+ try {
21827
+ rmSync(skillResourcesDir, { recursive: true, force: true });
21828
+ console.error(`[MCP-PIPE] Skill 资源目录已清理: ${skillResourcesDir}`);
21829
+ } catch (error2) {
21830
+ console.error(`[MCP-PIPE] 清理 Skill 资源目录失败: ${skillResourcesDir}`, error2);
21831
+ }
21832
+ }
21833
+ }
21514
21834
  async function main() {
21835
+ process.on("exit", cleanupSkillDir);
21836
+ process.on("SIGINT", () => {
21837
+ console.error("[MCP-PIPE] 收到 SIGINT,正在清理...");
21838
+ cleanupSkillDir();
21839
+ process.exit(0);
21840
+ });
21841
+ process.on("SIGTERM", () => {
21842
+ console.error("[MCP-PIPE] 收到 SIGTERM,正在清理...");
21843
+ cleanupSkillDir();
21844
+ process.exit(0);
21845
+ });
21846
+ if (!existsSync(skillResourcesDir)) {
21847
+ mkdirSync(skillResourcesDir, { recursive: true });
21848
+ }
21849
+ console.error(`[MCP-PIPE] Skill 资源目录: ${skillResourcesDir}`);
21515
21850
  await preloadSkills();
21851
+ await extractAllSkillResourcesToTempDir(skillResourcesDir);
21516
21852
  initMCPClients();
21517
21853
  const server = await createServer();
21518
21854
  const transport = new StdioServerTransport();
@@ -21521,5 +21857,6 @@ async function main() {
21521
21857
  }
21522
21858
  main().catch((error2) => {
21523
21859
  console.error("[MCP-PIPE] 致命错误:", error2);
21860
+ cleanupSkillDir();
21524
21861
  process.exit(1);
21525
21862
  });
@@ -20,7 +20,7 @@ version: 1.0.0
20
20
 
21
21
  ## 说明
22
22
 
23
- 本指令用于直接触发 Jenkins 构建,执行服务部署。与 `/ci` 命令不同,`/cd` 只负责部署,不负责代码合并。适用于仅需重新部署或部署特定分支的场景。
23
+ 本指令用于直接触发 Jenkins 构建,执行服务部署。与 `/ci` 命令不同,`/cd` 只负责部署。适用于仅需重新部署或部署特定分支的场景。
24
24
 
25
25
  > **⚠️ 重要约束**:
26
26
  > - 当 `config.yaml` 中设置 `jenkins.is_current_branch: false` 时,**严禁执行任何本地git命令**获取分支信息
@@ -83,7 +83,7 @@ version: 1.0.0
83
83
 
84
84
  > **⚠️ 配置读取优先级**:按以下顺序读取配置,找到即停止:
85
85
  > 1. 项目根目录的 `config.yaml`
86
- > 2. Skill 同级目录的 `template.yaml`
86
+ > 2. 默认模版配置 `./template.yaml`
87
87
  >
88
88
  > **必须每次重新读取**:禁止使用上下文缓存,每次执行时都要重新读取配置。
89
89
  >
@@ -104,10 +104,14 @@ version: 1.0.0
104
104
  - 若用户传入了 `branch:xxx`,提取指定分支名,**跳过分支获取**
105
105
  - 若未指定分支,继续下一步
106
106
 
107
- 3. **智能发现 Jenkins Job(优先执行)**:
108
- - 对每个环境,首先尝试在 `config.yaml` 中查找对应配置
109
- - **若配置存在**:使用配置中的 `fullname` 和 MCP 名称,进入参数处理流程(步骤 2)
107
+ 3. **查找 Jenkins Job(优先执行)**:
108
+ - 对每个环境,按以下顺序查找配置:
109
+ 1. 首先在 `config.yaml` 中查找对应配置
110
+ 2. 若 `config.yaml` 中不存在,在 `template.yaml` 中查找对应配置
111
+ - **若配置存在(config.yaml 或 template.yaml)**:使用配置中的 `fullname` 和 MCP 名称,直接进入参数处理流程(步骤 2),**无需用户确认**
110
112
  - **若配置不存在(智能发现模式)**:
113
+ - **智能发现定义**:只有 AI 主动去 Jenkins 上查找 Job 才算智能发现
114
+ - template.yaml 中的配置属于**预配置**,不算智能发现
111
115
  1. **扫描所有 Jenkins MCP**:
112
116
  - 读取 `mcp.json` 配置,识别所有可用的 Jenkins MCP(名称包含 `jenkins` 的 MCP)
113
117
  - 按 MCP 名称排序,优先尝试 `jenkins`,然后是 `jenkins-prod`、`jenkins-test` 等其他 Jenkins MCP
@@ -472,11 +476,3 @@ params:
472
476
  /cd dev-api,dev-admin branch:release/v1.0
473
477
  ```
474
478
 
475
- ## 与 /ci 命令的区别
476
-
477
- | 命令 | 功能 | 适用场景 |
478
- |------|------|----------|
479
- | `/ci` | MR 合并 + 可选 Jenkins 构建 | 需要代码合并并部署 |
480
- | `/cd` | 直接 Jenkins 构建 | 仅需部署,无需合并 |
481
-
482
- > **建议**:日常开发使用 `/ci` 完成合并+部署;若只想重新部署或部署特定分支,使用 `/cd`。
@@ -55,7 +55,11 @@ version: 1.0.0
55
55
  ### 0. 分支存在性检查逻辑
56
56
 
57
57
  创建 MR 前执行:
58
- - 调用 `mcp_gitlab_browse_refs` (action=get_branch, branch={目标分支})
58
+ - 调用 `mcp_gitlab_browse_refs` (action=get_branch, project_id={project_id}, branch={目标分支})
59
+ - **必需参数**:
60
+ - `action`: "get_branch"
61
+ - `project_id`: 项目 ID 或 URL 编码路径(如 "zhangkr/ai-test")
62
+ - `branch`: 分支名称
59
63
  - 如果返回 404 或分支不存在:**立即停止执行**,输出:
60
64
  ```
61
65
  ❌ 目标分支不存在:{目标分支}
@@ -65,11 +69,12 @@ version: 1.0.0
65
69
  ### 1. MR 状态检查逻辑
66
70
 
67
71
  创建 MR 后执行:
72
+ - **查询 MR 状态**:使用【查询 MR 工具】
68
73
  - 检查 `merge_status` 字段
69
74
  - 如果为 `cannot_be_merged`:
70
75
  1. **检查是否为无文件变更的情况**:比较 `diff_refs.base_sha` 和 `diff_refs.head_sha`
71
76
  - 如果两者相同(无实际文件变更):
72
- - 自动关闭 MR:`mcp_gitlab_manage_merge_request` (action=update, state_event=close)
77
+ - 执行【关闭 MR 逻辑】
73
78
  - 输出提示信息:
74
79
  ```
75
80
  ℹ️ MR 无文件变更(已合并过),自动关闭
@@ -93,8 +98,7 @@ version: 1.0.0
93
98
 
94
99
  ### 3. MR 合并逻辑
95
100
 
96
- 执行 `mcp_gitlab_manage_merge_request` (action=merge):
97
- - `merge_when_pipeline_succeeds: true`
101
+ 执行【合并 MR 工具】
98
102
  - 如果失败(如 405 错误):**立即停止执行**,输出:
99
103
  ```
100
104
  ❌ MR 合并失败:{error_message}
@@ -102,40 +106,38 @@ version: 1.0.0
102
106
  请检查 MR 状态后手动处理
103
107
  ```
104
108
 
105
- ## 执行流程
106
-
107
- ### 前置检查: GitLab MCP 可用性验证
109
+ ### 4. 关闭 MR 逻辑
108
110
 
109
- 在执行任何步骤前,必须先完成以下验证,**验证通过后才能继续,否则立即停止**:
111
+ 执行 `mcp_gitlab_manage_merge_request` (action=update, project_id={project_id}, merge_request_iid={iid}, source_branch={source_branch}, target_branch={target_branch}, state_event=close)
112
+ - **必需参数**:
113
+ - `action`: "update"
114
+ - `project_id`: 项目 ID 或 URL 编码路径
115
+ - `merge_request_iid`: MR 的内部 ID
116
+ - `source_branch`: 源分支名称
117
+ - `target_branch`: 目标分支名称
118
+ - `state_event`: "close"
110
119
 
111
- 1. 调用只读工具 `mcp_gitlab_browse_projects`(action=list, per_page=1)
112
- 2. **判断结果**:
113
- - 如果返回 `tool not found` 或 `not found MCPHost` 或 `50001` 错误码:**立即停止,禁止执行后续任何步骤**,输出:
114
- ```
115
- ❌ GitLab MCP 不可用,无法执行自动化 CI 流程
116
- 请在 MCP 配置中启用 GitLab MCP 后重试:
117
- 编辑器设置 → MCP → 启用 GitLab
118
- ```
119
- - 如果返回其他任何结果(含 404、401、数据等):视为 MCP 可用,继续执行
120
+ ## 执行流程
120
121
 
121
122
  ### 步骤 1: 获取分支信息
122
123
 
123
124
  > **⚠️ 配置读取优先级**:按以下顺序读取配置,找到即停止:
124
125
  > 1. 项目根目录的 `config.yaml`
125
- > 2. Skill 同级目录的 `template.yaml`
126
+ > 2. 默认模版配置(Skill 目录下的 `template.yaml`)
126
127
  >
127
128
  > **必须每次重新读取**:禁止使用上下文缓存,每次执行 `/ci` 时都要重新读取配置。
128
129
 
129
130
  **配置读取逻辑**:
130
- 1. 首先尝试读取项目根目录的 `config.yaml`
131
- 2. 若 `config.yaml` 不存在或读取失败,则读取本 Skill 目录下的 `template.yaml`
131
+ 1. 首先尝试读取项目根目录的 `config.yaml`(使用 `read_file` 工具)
132
+ 2. 若 `config.yaml` 不存在或读取失败,则读取 Skill 目录下的默认配置 `template.yaml`
133
+ - 使用 `read_file` 工具读取 Skill 目录下的 `template.yaml` 文件
132
134
  3. 合并配置:以读取到的配置为基础,缺失的值使用 `template.yaml` 中的默认值补充
133
135
 
134
136
  1. **解析 SOURCE_BRANCH(源分支)和项目路径**:
135
137
  - 若用户传入了 `branch:分支名`,直接使用该分支作为 SOURCE_BRANCH
136
138
  - 若 `config.yaml` 中配置了 `projects.url`,从 URL 中解析项目路径
137
139
  - **若以上两者都未满足**,合并执行以下命令(一次终端调用获取全部信息):
138
- 执行 `git branch --show-current && git remote get-url origin`,解析项目路径(如 zhangkr/ai-test)
140
+ 执行 `git branch --show-current && git remote get-url origin`,解析项目路径
139
141
  - 第一行输出作为 SOURCE_BRANCH
140
142
  - 第二行输出解析为项目路径
141
143
 
@@ -153,12 +155,13 @@ version: 1.0.0
153
155
 
154
156
  1. **执行【分支存在性检查逻辑】**(目标分支:{PERSONAL_BRANCH})
155
157
 
156
- 2. **创建 MR**:`mcp_gitlab_manage_merge_request` (action=create)
158
+ 2. **创建 MR**:执行【创建 MR 工具】
157
159
  - source_branch: {SOURCE_BRANCH}
158
160
  - target_branch: {PERSONAL_BRANCH}
159
- - title: `{mr_title_template}`(模板变量:{source_branch}, {target_branch})
161
+ - 记录返回的 MR IID(`iid` 字段),用于后续查询
160
162
 
161
- 3. **执行【MR 状态检查逻辑】**
163
+ 3. **执行【MR 状态检查】**
164
+ - 使用步骤 2 中获取的 MR IID 查询状态
162
165
 
163
166
  4. **执行【Review 检查逻辑】**(如果启用 Review 模式)
164
167
 
@@ -172,12 +175,13 @@ version: 1.0.0
172
175
 
173
176
  1. **执行【分支存在性检查逻辑】**(目标分支:{TARGET_BRANCH})
174
177
 
175
- 2. **创建 MR**:`mcp_gitlab_manage_merge_request` (action=create)
176
- - source_branch: 根据上述规则确定
178
+ 2. **创建 MR**:执行【创建 MR 工具】
179
+ - source_branch: 根据规则确定({PERSONAL_BRANCH} 或 {SOURCE_BRANCH})
177
180
  - target_branch: {TARGET_BRANCH}
178
- - title: `{mr_title_template}`(模板变量:{source_branch}, {target_branch})
181
+ - 记录返回的 MR IID(`iid` 字段),用于后续查询
179
182
 
180
183
  3. **执行【MR 状态检查逻辑】**
184
+ - 使用步骤 2 中获取的 MR IID 查询状态
181
185
 
182
186
  4. **执行【Review 检查逻辑】**(如果启用 Review 模式)
183
187
 
@@ -227,8 +231,13 @@ version: 1.0.0
227
231
  2. **按照 CD skill 的规范执行部署流程**:
228
232
  - 执行 Jenkins MCP 可用性验证
229
233
  - 读取 `config.yaml` 中的 Jenkins 配置
230
- - 使用 `mcp_jenkins_build_item` 触发构建
234
+ - 使用 `mcp_{jenkins_name}_build_item` 触发构建(如 `mcp_jenkins_build_item` 或 `mcp_jenkins_prod_build_item`)
231
235
  - **⚠️ 关键:`build_type` 必须为 `buildWithParameters`**
236
+ - **必需参数**:
237
+ - `fullname`: Jenkins Job 全名
238
+ - `build_type`: "buildWithParameters"
239
+ - `params`: 构建参数对象(如 `{BUILD_NAME: "build:dev:api"}`)
240
+ - **响应说明**:返回队列 ID(如 107941)
232
241
 
233
242
  3. **多环境部署**:按环境顺序逐个触发,记录每个环境的队列 ID
234
243
 
@@ -332,3 +341,118 @@ version: 1.0.0
332
341
  # 方式2:一体化执行
333
342
  /ci dev 发布 # 合并 + 部署一步完成
334
343
  ```
344
+
345
+ ---
346
+
347
+ ## 附录:工具参数参考
348
+
349
+ ### 公共工具定义
350
+
351
+ 以下工具在多个步骤中被复用,统一在此定义:
352
+
353
+ #### 【查询 MR 工具】
354
+ `mcp_gitlab_browse_merge_requests` - 查询 MR 状态
355
+ - **必需参数**:
356
+ - `action`: "get"
357
+ - `project_id`: 项目 ID 或 URL 编码路径
358
+ - `merge_request_iid`: MR 内部 ID
359
+ - `per_page`: 每页数量(如 20)
360
+ - **关键响应字段**:
361
+ - `merge_status`: 合并状态("can_be_merged")
362
+ - `state`: MR 状态("opened"/"merged"/"closed")
363
+ - `diff_refs.base_sha`: 基础提交 SHA
364
+ - `diff_refs.head_sha`: 头部提交 SHA
365
+ - `web_url`: MR 页面链接
366
+
367
+ #### 【创建 MR 工具】
368
+ `mcp_gitlab_manage_merge_request` - 创建新的合并请求
369
+ - **必需参数**:
370
+ - `action`: "create"
371
+ - `project_id`: 项目 ID 或 URL 编码路径
372
+ - `source_branch`: 源分支名称
373
+ - `target_branch`: 目标分支名称
374
+ - `title`: MR 标题(支持模板变量)
375
+ - `description`: MR 描述(可选)
376
+ - **关键响应字段**:
377
+ - `iid`: MR 内部 ID(必需记录)
378
+ - `web_url`: MR 页面链接
379
+ - `merge_status`: 合并状态
380
+
381
+ #### 【合并 MR 工具】
382
+ `mcp_gitlab_manage_merge_request` - 合并 MR
383
+ - **必需参数**:
384
+ - `action`: "merge"
385
+ - `project_id`: 项目 ID 或 URL 编码路径
386
+ - `merge_request_iid`: MR 内部 ID
387
+ - `source_branch`: 源分支名称
388
+ - `target_branch`: 目标分支名称
389
+ - `merge_when_pipeline_succeeds`: true
390
+
391
+ ---
392
+
393
+ ### GitLab 工具详情
394
+
395
+ #### 1. mcp_gitlab_browse_refs
396
+ 查询分支信息。
397
+
398
+ **参数**:
399
+ | 参数名 | 类型 | 必需 | 说明 |
400
+ |--------|------|------|------|
401
+ | `action` | string | ✅ | 固定值 "get_branch" |
402
+ | `project_id` | string | ✅ | 项目 ID 或 URL 编码路径(如 "zhangkr/ai-test") |
403
+ | `branch` | string | ✅ | 分支名称 |
404
+
405
+ **响应字段**:
406
+ | 字段名 | 说明 |
407
+ |--------|------|
408
+ | `name` | 分支名称 |
409
+ | `commit.id` | 最新提交 SHA |
410
+ | `commit.title` | 最新提交标题 |
411
+ | `merged` | 是否已合并 |
412
+ | `protected` | 是否受保护 |
413
+
414
+ #### 2. mcp_gitlab_browse_merge_requests
415
+ 查询 MR 信息。详见【查询 MR 工具】定义。
416
+
417
+ **额外说明**:
418
+ - `action=list` 时不需要 `merge_request_iid`
419
+ - 支持分页参数:`page`, `per_page`
420
+
421
+ #### 3. mcp_gitlab_manage_merge_request
422
+ 管理 MR(创建、更新、合并)。详见【创建 MR 工具】、【合并 MR 工具】、【关闭 MR 逻辑】定义。
423
+
424
+ **update 专属参数**(关闭/重新打开 MR):
425
+ | 参数名 | 类型 | 必需 | 说明 |
426
+ |--------|------|------|------|
427
+ | `merge_request_iid` | number | ✅ | MR 内部 ID |
428
+ | `source_branch` | string | ✅ | 源分支名称 |
429
+ | `target_branch` | string | ✅ | 目标分支名称 |
430
+ | `state_event` | string | ✅ | "close" 或 "reopen" |
431
+
432
+ ### Jenkins 工具
433
+
434
+ #### mcp_{jenkins_name}_build_item
435
+ 触发 Jenkins 构建。
436
+
437
+ **参数**:
438
+ | 参数名 | 类型 | 必需 | 说明 |
439
+ |--------|------|------|------|
440
+ | `fullname` | string | ✅ | Jenkins Job 全名 |
441
+ | `build_type` | string | ✅ | 固定值 "buildWithParameters" |
442
+ | `params` | object | ✅ | 构建参数对象(如 `{BUILD_NAME: "build:dev:api"}`) |
443
+
444
+ **响应**:
445
+ - 成功:返回队列 ID(number,如 107941)
446
+ - 失败:返回错误信息
447
+
448
+ #### mcp_{jenkins_name}_get_all_items
449
+ 获取所有 Jenkins Jobs(用于可用性验证)。
450
+
451
+ **参数**:
452
+ | 参数名 | 类型 | 必需 | 说明 |
453
+ |--------|------|------|------|
454
+ | `random_string` | string | ✅ | 任意字符串(如 "check") |
455
+
456
+ **响应**:
457
+ - 成功:返回 Job 列表数组
458
+ - 失败:返回错误信息
@@ -24,6 +24,19 @@ templates:
24
24
  - 目标分支: {target_branch}
25
25
  - 触发环境: {env}
26
26
  - 触发时间: {timestamp}
27
+ jenkins:
28
+ is_current_branch: false
29
+ '广告主开发':
30
+ fullname: "dev-platforms-advertiser-140.227"
31
+ url: "http://10.169.140.235:30866/view/双统前台/job/dev-platforms-advertiser-140.227/"
32
+ '广告主测试':
33
+ fullname: "test-platforms-advertiser-140.227"
34
+ url: "http://10.169.140.235:30866/view/双统前台/job/test-platforms-advertiser-140.227/"
35
+ jenkins-prod:
36
+ is_current_branch: false
37
+ '广告主准生产':
38
+ fullname: "uat-gtqt-platforms-advertiser-150.27"
39
+ url: "http://10.168.171.71:28080/view/uat-双统前台/job/uat-platforms-advertiser-150.27/"
27
40
 
28
41
  defaults:
29
42
  env: "dev" # 默认环境
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postar-pipe-mcp",
3
- "version": "0.0.2",
3
+ "version": "1.0.0",
4
4
  "description": "Unified CI/CD MCP Server - orchestrates GitLab and Jenkins MCPs with dynamic skills",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",