mcp-osp-prompt 1.0.2 → 1.0.4

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/dialog/native.js CHANGED
@@ -1,6 +1,32 @@
1
1
  import os from 'os';
2
- import { execSync } from 'child_process';
2
+ import { execSync, exec } from 'child_process';
3
+ import { promisify } from 'util';
3
4
  import { escapeAppleScriptString } from './utils.js';
5
+
6
+ // 使用 promisify 将 exec 转为 Promise 版本,避免阻塞事件循环
7
+ const execAsync = promisify(exec);
8
+
9
+ // 🔧 JSON-RPC 心跳函数:通过 stdout 发送 notification,防止 MCP 客户端超时
10
+ let heartbeatCounter = 0;
11
+ function sendHeartbeat(context = 'dialog') {
12
+ heartbeatCounter++;
13
+
14
+ // 尝试使用 MCP 标准的 progress notification 格式
15
+ const notification = {
16
+ jsonrpc: '2.0',
17
+ method: 'notifications/progress',
18
+ params: {
19
+ progressToken: `heartbeat-${Date.now()}`,
20
+ progress: heartbeatCounter,
21
+ total: -1 // -1 表示未知总数
22
+ }
23
+ };
24
+
25
+ // 写入 stdout,这是 MCP 客户端监听的通道
26
+ process.stdout.write(JSON.stringify(notification) + '\n');
27
+ // 同时写入 stderr 供调试(只保留一行日志)
28
+ console.error(`[Heartbeat #${heartbeatCounter}] progress notification sent`);
29
+ }
4
30
  // 🟢 GREEN: Task 2.2 - 导入CFG配置用于native弹窗宽度设置
5
31
  import { CFG } from '../config.js';
6
32
  // ✅ UNIFIED: 导入统一的按钮管理常量
@@ -63,16 +89,21 @@ export async function showInputDialog({ title = '输入', message = '请输入
63
89
  end if
64
90
  `;
65
91
 
66
- // 🆕 添加保活机制
92
+ // 🆕 添加保活机制(5秒间隔,通过 stdout 发送 JSON-RPC notification)
67
93
  const keepAliveInterval = setInterval(() => {
68
- console.error('[Dialog-KeepAlive] Waiting for user input in textarea...');
69
- }, 15000);
94
+ sendHeartbeat('input');
95
+ }, 5000);
70
96
 
71
97
  try {
72
- const result = execSync(`osascript -e '${applescript}'`, {
98
+ console.error('[Dialog] Calling osascript for input dialog with async exec (non-blocking)...');
99
+
100
+ // 🔧 关键修改:使用异步 exec 替代 execSync,避免阻塞事件循环
101
+ const { stdout } = await execAsync(`osascript -e '${applescript}'`, {
73
102
  encoding: 'utf8',
74
- timeout: 1800000 // 30 minutes timeout (用户要求增加timeout)
75
- }).trim();
103
+ timeout: 1800000 // 30 minutes timeout
104
+ });
105
+
106
+ const result = stdout.trim();
76
107
 
77
108
  clearInterval(keepAliveInterval); // 清理定时器
78
109
 
@@ -159,13 +190,13 @@ export async function showConfirmDialog({ title = '确认', message = '请选择
159
190
 
160
191
  const platform = os.platform();
161
192
 
162
- // 🆕 添加保活机制
193
+ // 🆕 添加保活机制(5秒间隔,通过 stdout 发送 JSON-RPC notification)
163
194
  const keepAliveInterval = setInterval(() => {
164
- console.error('[Dialog-KeepAlive] Waiting for user to select button...');
165
- }, 15000);
195
+ sendHeartbeat('button selection');
196
+ }, 5000);
166
197
 
167
198
  try {
168
- let command, args;
199
+ let result;
169
200
  if (platform === 'darwin') {
170
201
  // Use enhanced escaping for all text content to handle special characters safely
171
202
  const safeTitle = escapeAppleScriptString(title).trim() || 'Confirmation';
@@ -183,31 +214,31 @@ export async function showConfirmDialog({ title = '确认', message = '请选择
183
214
  const buttonsList = safeButtons.map(b => `"${b}"`).join(', ');
184
215
  const script = `set selectedButton to button returned of (display dialog "${safeMessage}" with title "${safeTitle}" buttons {${buttonsList}} default button "${safeButtons[0]}")`;
185
216
 
186
- // Removed: DEBUG logs with array/object output (causes JSON parse errors in MCP client)
217
+ console.error('[Dialog] Calling osascript with async exec (non-blocking)...');
187
218
 
188
- const result = execSync(`osascript -e '${script}'`, {
219
+ // 🔧 关键修改:使用异步 exec 替代 execSync,避免阻塞事件循环
220
+ const { stdout } = await execAsync(`osascript -e '${script}'`, {
189
221
  encoding: 'utf8',
190
- timeout: 1800000, // 30 minutes timeout (用户要求增加timeout)
191
- stdio: ['pipe', 'pipe', 'pipe']
222
+ timeout: 3600000 // 60 minutes timeout
192
223
  });
193
224
 
194
225
  clearInterval(keepAliveInterval); // 清理定时器
195
226
 
196
- const selectedButton = result.trim();
227
+ const selectedButton = stdout.trim();
197
228
 
198
229
  // Map safe button back to original button
199
230
  const safeIndex = safeButtons.indexOf(selectedButton);
200
231
  return safeIndex >= 0 ? buttons[safeIndex] : buttons[0];
201
232
 
202
233
  } else {
203
- // Linux zenity
234
+ // Linux zenity - 也改为异步
204
235
  console.log('🐧 [ZENITY] Showing dialog...');
205
- const result = execSync(`zenity --question --title="${title}" --text="${message}" --ok-label="${buttons[0]}" --cancel-label="${buttons[1]}"`, {
236
+ const { stdout } = await execAsync(`zenity --question --title="${title}" --text="${message}" --ok-label="${buttons[0]}" --cancel-label="${buttons[1]}"`, {
206
237
  encoding: 'utf8',
207
- timeout: 300000
238
+ timeout: 3600000 // 60 minutes timeout
208
239
  });
209
240
  clearInterval(keepAliveInterval); // 清理定时器
210
- return result.trim() || buttons[0];
241
+ return stdout.trim() || buttons[0];
211
242
  }
212
243
  } catch (error) {
213
244
  clearInterval(keepAliveInterval); // 出错时也要清理
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-osp-prompt",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "MCP server for fetching and caching prompt templates from GitHub, GitLab, or local directories",
5
5
  "main": "server.js",
6
6
  "bin": {
package/prompt-manager.js CHANGED
@@ -308,6 +308,22 @@ class PromptManager {
308
308
  },
309
309
  required: ['random_string']
310
310
  }
311
+ },
312
+ {
313
+ name: 'gitlab-mr-fetch',
314
+ type: 'handler',
315
+ handler: 'handleGitlabMrFetch',
316
+ description: 'Fetch GitLab Merge Request details and code diff',
317
+ schema: {
318
+ type: 'object',
319
+ properties: {
320
+ mr_url: {
321
+ type: 'string',
322
+ description: 'GitLab Merge Request URL, e.g., https://gitlab.com/group/project/-/merge_requests/123'
323
+ }
324
+ },
325
+ required: ['mr_url']
326
+ }
311
327
  }
312
328
  ];
313
329
 
package/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import process from 'process';
3
3
  import { initializePrompts, getToolsConfiguration } from './prompt-manager.js';
4
- import { handleDevTool, handleDevManual, handleDevFeedback } from './tools.js';
4
+ import { handleDevTool, handleDevManual, handleDevFeedback, handleGitlabMrFetch } from './tools.js';
5
5
  import { CFG } from './config.js';
6
6
  import {
7
7
  createSuccessResponse,
@@ -64,10 +64,10 @@ async function initializeServerSync() {
64
64
  try {
65
65
  console.error('[MCP-Server] Starting synchronous initialization...');
66
66
 
67
- // 每10秒输出一次日志,保持连接活跃
67
+ // 每5秒输出一次日志,保持连接活跃(防止客户端超时)
68
68
  keepAliveInterval = setInterval(() => {
69
- console.error('[MCP-KeepAlive] Initializing tools, please wait...');
70
- }, 10000);
69
+ console.error('[MCP-KeepAlive] I\'m alive, Heartbeat, Heartbeat...');
70
+ }, 5000);
71
71
 
72
72
  // 同步初始化prompt管理器
73
73
  globalToolsConfig = await initializePrompts();
@@ -233,6 +233,9 @@ async function handleToolCall(req) {
233
233
  ⚠️ **MANDATORY NEXT STEP REMINDER:** 在完成下一个实施步骤后,必须再次调用dev-feedback工具汇报进度并获取确认。每个步骤完成后都需要调用feedback工具 - 这是强制要求!`;
234
234
  }
235
235
  break;
236
+ case 'handleGitlabMrFetch':
237
+ resultText = await handleGitlabMrFetch(args);
238
+ break;
236
239
  default:
237
240
  throw new Error(`Unknown handler: ${tool.handler}`);
238
241
  }
package/tools.js CHANGED
@@ -3,6 +3,7 @@ import { getToolsConfiguration, getLocalPromptInfo } from './prompt-manager.js';
3
3
  import { showInputDialog, showConfirmDialog, showSelectDialog, showPlanAdjustmentDialog } from './dialog/index.js';
4
4
  // ✅ UNIFIED: 导入统一的按钮管理常量
5
5
  import { BASE_DIALOG_BUTTONS, getSafeButtons, getStandardButtons } from './dialog/constants.js';
6
+ import { createAuthHeaders } from './platform-utils.js';
6
7
 
7
8
  // 动态获取工具的source映射
8
9
  function getToolSource(toolName) {
@@ -478,12 +479,17 @@ export async function handleDevManual() {
478
479
  }
479
480
 
480
481
  export async function handleDevFeedback(args) {
482
+ console.error('[DEBUG] handleDevFeedback called with args:', JSON.stringify(args, null, 2));
483
+
481
484
  // Support both old simple format and new enhanced format
482
485
  if (args.phase || args.context || args.allowPlanAdjustment) {
486
+ console.error('[DEBUG] Taking enhanced feedback branch');
483
487
  // Use enhanced feedback for new format
484
488
  return await enhancedDevFeedback(args);
485
489
  }
486
490
 
491
+ console.error('[DEBUG] Taking legacy feedback branch');
492
+
487
493
  // Legacy support for old format
488
494
  const { title, message, options = [] } = args;
489
495
  if (process.env.AUTOMATED_MODE === 'true') {
@@ -495,10 +501,7 @@ export async function handleDevFeedback(args) {
495
501
  // 简化逻辑:直接传入options,函数内部自动判断场景
496
502
  const finalOptions = getStandardButtons(options.length > 0 ? options : null);
497
503
 
498
- // 🆕 添加保活机制:在等待用户交互期间定期输出日志
499
- const keepAliveInterval = setInterval(() => {
500
- console.error('[MCP-KeepAlive] Waiting for user dialog interaction...');
501
- }, 15000);
504
+ console.error('[DEBUG] Calling showConfirmDialog (heartbeat is handled in native.js)...');
502
505
 
503
506
  try {
504
507
  const result = await showConfirmDialog({
@@ -507,8 +510,6 @@ export async function handleDevFeedback(args) {
507
510
  buttons: finalOptions
508
511
  });
509
512
 
510
- clearInterval(keepAliveInterval); // 用户选择后清理定时器
511
-
512
513
  // If user selects a "modify" option, automatically show input dialog
513
514
  if (result && (result.includes('修改') || result.includes('调整') || result.includes('建议'))) {
514
515
 
@@ -530,7 +531,6 @@ export async function handleDevFeedback(args) {
530
531
  }
531
532
  return result;
532
533
  } catch (error) {
533
- clearInterval(keepAliveInterval); // 出错时也要清理定时器
534
534
  console.error('❌ [FEEDBACK ERROR]:', error.message);
535
535
 
536
536
  // Check if this is a dialog system failure
@@ -565,4 +565,127 @@ export async function handleDevFeedback(args) {
565
565
  }
566
566
 
567
567
  // handleDevReload removed - configuration now loads automatically from environment and mcp.json
568
- // No manual reload needed as per user requirements
568
+ // No manual reload needed as per user requirements
569
+
570
+ /**
571
+ * Parse GitLab MR URL to extract project and MR IID
572
+ * @param {string} mrUrl - GitLab MR URL
573
+ * @returns {object} Parsed info { projectPath, mrIid, apiBase }
574
+ */
575
+ function parseGitlabMrUrl(mrUrl) {
576
+ // Match: https://gitlab.com/group/project/-/merge_requests/123
577
+ // Or: https://gitlab.example.com/group/subgroup/project/-/merge_requests/123
578
+ const match = mrUrl.match(/^(https?:\/\/[^\/]+)\/(.+?)\/-\/merge_requests\/(\d+)/);
579
+
580
+ if (!match) {
581
+ throw new Error(`Invalid GitLab MR URL format: ${mrUrl}. Expected format: https://gitlab.com/group/project/-/merge_requests/123`);
582
+ }
583
+
584
+ return {
585
+ apiBase: match[1],
586
+ projectPath: match[2],
587
+ mrIid: match[3]
588
+ };
589
+ }
590
+
591
+ /**
592
+ * Fetch GitLab Merge Request details and code diff
593
+ * @param {object} args - Arguments with mr_url
594
+ * @returns {string} Formatted MR information with diffs
595
+ */
596
+ export async function handleGitlabMrFetch(args) {
597
+ const { mr_url } = args;
598
+
599
+ if (!mr_url) {
600
+ throw new Error('mr_url is required');
601
+ }
602
+
603
+ const token = process.env.GIT_TOKEN;
604
+ if (!token) {
605
+ throw new Error('GIT_TOKEN environment variable is required for GitLab API access');
606
+ }
607
+
608
+ const { apiBase, projectPath, mrIid } = parseGitlabMrUrl(mr_url);
609
+ const projectEnc = encodeURIComponent(projectPath);
610
+ const headers = createAuthHeaders('gitlab', token);
611
+
612
+ console.error(`[GitLab MR] Fetching MR ${mrIid} from ${projectPath}`);
613
+
614
+ try {
615
+ // Fetch MR details
616
+ const mrResponse = await fetch(
617
+ `${apiBase}/api/v4/projects/${projectEnc}/merge_requests/${mrIid}`,
618
+ { headers }
619
+ );
620
+
621
+ if (!mrResponse.ok) {
622
+ const errorText = await mrResponse.text();
623
+ throw new Error(`GitLab API error (${mrResponse.status}): ${errorText}`);
624
+ }
625
+
626
+ const mrData = await mrResponse.json();
627
+
628
+ // Fetch MR changes (diffs)
629
+ const changesResponse = await fetch(
630
+ `${apiBase}/api/v4/projects/${projectEnc}/merge_requests/${mrIid}/changes`,
631
+ { headers }
632
+ );
633
+
634
+ if (!changesResponse.ok) {
635
+ const errorText = await changesResponse.text();
636
+ throw new Error(`GitLab API error fetching changes (${changesResponse.status}): ${errorText}`);
637
+ }
638
+
639
+ const changesData = await changesResponse.json();
640
+
641
+ // Format the output
642
+ let output = `# GitLab Merge Request Details
643
+
644
+ ## Basic Information
645
+ - **Title:** ${mrData.title}
646
+ - **MR IID:** !${mrData.iid}
647
+ - **Author:** ${mrData.author?.name || 'Unknown'} (@${mrData.author?.username || 'unknown'})
648
+ - **Source Branch:** ${mrData.source_branch}
649
+ - **Target Branch:** ${mrData.target_branch}
650
+ - **State:** ${mrData.state}
651
+ - **Created At:** ${mrData.created_at}
652
+ - **Updated At:** ${mrData.updated_at}
653
+ - **URL:** ${mr_url}
654
+
655
+ ## Description
656
+ ${mrData.description || '_No description provided_'}
657
+
658
+ ## Changed Files (${changesData.changes?.length || 0} files)
659
+
660
+ `;
661
+
662
+ // Add file changes
663
+ if (changesData.changes && changesData.changes.length > 0) {
664
+ for (const change of changesData.changes) {
665
+ const status = change.new_file ? '🆕 New' :
666
+ change.deleted_file ? '🗑️ Deleted' :
667
+ change.renamed_file ? '📝 Renamed' : '✏️ Modified';
668
+
669
+ output += `### ${status}: \`${change.new_path}\`\n`;
670
+
671
+ if (change.renamed_file && change.old_path !== change.new_path) {
672
+ output += `_Renamed from: ${change.old_path}_\n`;
673
+ }
674
+
675
+ output += '\n```diff\n';
676
+ output += change.diff || '_Binary file or no diff available_';
677
+ output += '\n```\n\n';
678
+ }
679
+ } else {
680
+ output += '_No changes found_\n';
681
+ }
682
+
683
+ console.error(`[GitLab MR] Successfully fetched MR with ${changesData.changes?.length || 0} changed files`);
684
+
685
+ return output;
686
+
687
+ } catch (error) {
688
+ console.error(`[GitLab MR] Error:`, error.message);
689
+ throw error;
690
+ }
691
+ }