ring-skills-mcp 1.2.2 → 1.3.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.
Files changed (2) hide show
  1. package/dist/index.js +630 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,6 +6,311 @@ import { exec } from "child_process";
6
6
  import { promisify } from "util";
7
7
  import path from "path";
8
8
  const execAsync = promisify(exec);
9
+ /**
10
+ * Login to GitLab and create a Personal Access Token
11
+ * This simulates the browser login flow to obtain an access token
12
+ */
13
+ async function loginToGitLabAndCreateToken(host, username, password, tokenName = "ring-skills-mcp-auto") {
14
+ const baseUrl = `https://${host}`;
15
+ try {
16
+ // Step 1: Get the sign-in page to extract CSRF token
17
+ const signInPageResponse = await fetch(`${baseUrl}/users/sign_in`, {
18
+ method: "GET",
19
+ headers: {
20
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
21
+ "Accept": "text/html,application/xhtml+xml",
22
+ },
23
+ redirect: "manual",
24
+ });
25
+ const signInHtml = await signInPageResponse.text();
26
+ // Extract CSRF token from the page
27
+ const csrfMatch = signInHtml.match(/name="authenticity_token"\s+value="([^"]+)"/);
28
+ if (!csrfMatch) {
29
+ return { success: false, error: "Could not get CSRF token from login page. GitLab page structure may have changed." };
30
+ }
31
+ const csrfToken = csrfMatch[1];
32
+ // Get cookies from sign-in page
33
+ const cookies = signInPageResponse.headers.get("set-cookie") || "";
34
+ // Step 2: Submit login form
35
+ const loginFormData = new URLSearchParams({
36
+ "authenticity_token": csrfToken,
37
+ "user[login]": username,
38
+ "user[password]": password,
39
+ "user[remember_me]": "0",
40
+ });
41
+ const loginResponse = await fetch(`${baseUrl}/users/sign_in`, {
42
+ method: "POST",
43
+ headers: {
44
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
45
+ "Content-Type": "application/x-www-form-urlencoded",
46
+ "Accept": "text/html,application/xhtml+xml",
47
+ "Cookie": cookies,
48
+ "Referer": `${baseUrl}/users/sign_in`,
49
+ },
50
+ body: loginFormData.toString(),
51
+ redirect: "manual",
52
+ });
53
+ // Check if login was successful (usually redirects to root or dashboard)
54
+ const loginStatus = loginResponse.status;
55
+ const loginCookies = loginResponse.headers.get("set-cookie") || "";
56
+ const allCookies = combineCookies(cookies, loginCookies);
57
+ if (loginStatus !== 302 && loginStatus !== 303) {
58
+ const loginHtml = await loginResponse.text();
59
+ if (loginHtml.includes("Invalid login or password") || loginHtml.includes("Invalid Login or password")) {
60
+ return { success: false, error: "Invalid username or password" };
61
+ }
62
+ if (loginHtml.includes("Two-Factor Authentication") || loginHtml.includes("two_factor")) {
63
+ return { success: false, error: "This account has Two-Factor Authentication (2FA) enabled. Cannot auto-login. Please create a Personal Access Token manually." };
64
+ }
65
+ return { success: false, error: `Login failed with status code: ${loginStatus}` };
66
+ }
67
+ // Step 3: Access the personal access tokens page to get new CSRF token
68
+ const tokensPageResponse = await fetch(`${baseUrl}/-/user_settings/personal_access_tokens`, {
69
+ method: "GET",
70
+ headers: {
71
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
72
+ "Accept": "text/html,application/xhtml+xml",
73
+ "Cookie": allCookies,
74
+ },
75
+ redirect: "follow",
76
+ });
77
+ const tokensHtml = await tokensPageResponse.text();
78
+ // Check if we're actually logged in
79
+ if (tokensHtml.includes("users/sign_in") || tokensHtml.includes("You need to sign in")) {
80
+ return { success: false, error: "Login session invalid. Please check your username and password." };
81
+ }
82
+ // Extract new CSRF token for creating token
83
+ const tokenCsrfMatch = tokensHtml.match(/name="authenticity_token"\s+value="([^"]+)"/);
84
+ if (!tokenCsrfMatch) {
85
+ // Try alternative pattern
86
+ const metaCsrfMatch = tokensHtml.match(/<meta\s+name="csrf-token"\s+content="([^"]+)"/);
87
+ if (!metaCsrfMatch) {
88
+ return { success: false, error: "Could not get CSRF token from token creation page" };
89
+ }
90
+ }
91
+ const tokenCsrf = tokenCsrfMatch ? tokenCsrfMatch[1] : tokensHtml.match(/<meta\s+name="csrf-token"\s+content="([^"]+)"/)[1];
92
+ // Update cookies
93
+ const tokenPageCookies = tokensPageResponse.headers.get("set-cookie") || "";
94
+ const finalCookies = combineCookies(allCookies, tokenPageCookies);
95
+ // Step 4: Create new Personal Access Token
96
+ // Token expires in 1 year
97
+ const expiresAt = new Date();
98
+ expiresAt.setFullYear(expiresAt.getFullYear() + 1);
99
+ const expiresAtStr = expiresAt.toISOString().split("T")[0];
100
+ const createTokenData = new URLSearchParams({
101
+ "authenticity_token": tokenCsrf,
102
+ "personal_access_token[name]": `${tokenName}-${Date.now()}`,
103
+ "personal_access_token[expires_at]": expiresAtStr,
104
+ "personal_access_token[scopes][]": "read_repository",
105
+ });
106
+ // Add additional scopes that might be needed
107
+ createTokenData.append("personal_access_token[scopes][]", "read_api");
108
+ const createTokenResponse = await fetch(`${baseUrl}/-/user_settings/personal_access_tokens`, {
109
+ method: "POST",
110
+ headers: {
111
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
112
+ "Content-Type": "application/x-www-form-urlencoded",
113
+ "Accept": "text/html,application/xhtml+xml",
114
+ "Cookie": finalCookies,
115
+ "Referer": `${baseUrl}/-/user_settings/personal_access_tokens`,
116
+ },
117
+ body: createTokenData.toString(),
118
+ redirect: "follow",
119
+ });
120
+ const createTokenHtml = await createTokenResponse.text();
121
+ // Extract the newly created token from the response
122
+ // GitLab shows the token in a specific element after creation
123
+ const newTokenMatch = createTokenHtml.match(/id="created-personal-access-token"[^>]*value="([^"]+)"/);
124
+ if (!newTokenMatch) {
125
+ // Try alternative pattern - look for the token in a flash message or specific div
126
+ const altTokenMatch = createTokenHtml.match(/new_token['"]\s*:\s*['"]([^'"]+)['"]/);
127
+ if (!altTokenMatch) {
128
+ // Try to find in clipboard copy button
129
+ const clipboardMatch = createTokenHtml.match(/data-clipboard-text="(glpat-[^"]+)"/);
130
+ if (!clipboardMatch) {
131
+ // Check if there's an error message
132
+ if (createTokenHtml.includes("error") || createTokenHtml.includes("Error")) {
133
+ return { success: false, error: "Failed to create token. A token with the same name may already exist or you may lack permissions." };
134
+ }
135
+ return { success: false, error: "Token was created but could not be extracted. Please copy it manually from the GitLab page." };
136
+ }
137
+ return { success: true, token: clipboardMatch[1] };
138
+ }
139
+ return { success: true, token: altTokenMatch[1] };
140
+ }
141
+ return { success: true, token: newTokenMatch[1] };
142
+ }
143
+ catch (error) {
144
+ if (error instanceof Error) {
145
+ if (error.message.includes("ENOTFOUND") || error.message.includes("getaddrinfo")) {
146
+ return { success: false, error: `Cannot connect to ${host}. Please check if the domain is correct.` };
147
+ }
148
+ if (error.message.includes("ECONNREFUSED")) {
149
+ return { success: false, error: `Connection refused. ${host} may not be accessible.` };
150
+ }
151
+ return { success: false, error: `Login error: ${error.message}` };
152
+ }
153
+ return { success: false, error: "Unknown error" };
154
+ }
155
+ }
156
+ /**
157
+ * Combine multiple Set-Cookie headers
158
+ */
159
+ function combineCookies(...cookieHeaders) {
160
+ const cookies = {};
161
+ for (const header of cookieHeaders) {
162
+ if (!header)
163
+ continue;
164
+ // Split multiple cookies
165
+ const parts = header.split(/,(?=\s*[^;]+=[^;]+)/);
166
+ for (const part of parts) {
167
+ const cookiePart = part.split(";")[0].trim();
168
+ const eqIndex = cookiePart.indexOf("=");
169
+ if (eqIndex > 0) {
170
+ const name = cookiePart.substring(0, eqIndex);
171
+ const value = cookiePart.substring(eqIndex + 1);
172
+ cookies[name] = value;
173
+ }
174
+ }
175
+ }
176
+ return Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
177
+ }
178
+ /**
179
+ * Open URL in default browser
180
+ */
181
+ async function openInBrowser(url) {
182
+ const platform = process.platform;
183
+ let command;
184
+ if (platform === "darwin") {
185
+ command = `open "${url}"`;
186
+ }
187
+ else if (platform === "win32") {
188
+ command = `start "" "${url}"`;
189
+ }
190
+ else {
191
+ command = `xdg-open "${url}"`;
192
+ }
193
+ await execAsync(command);
194
+ }
195
+ /**
196
+ * Check if error message indicates authentication failure
197
+ */
198
+ function isAuthenticationError(errorMessage) {
199
+ const authErrorPatterns = [
200
+ /authentication failed/i,
201
+ /could not read username/i,
202
+ /could not read password/i,
203
+ /invalid credentials/i,
204
+ /401/i,
205
+ /403/i,
206
+ /permission denied/i,
207
+ /access denied/i,
208
+ /fatal: repository .* not found/i,
209
+ /fatal: could not read from remote repository/i,
210
+ ];
211
+ return authErrorPatterns.some(pattern => pattern.test(errorMessage));
212
+ }
213
+ /**
214
+ * Check if git credentials exist for a given host
215
+ */
216
+ async function checkGitCredentials(host) {
217
+ try {
218
+ const input = `protocol=https\nhost=${host}\n\n`;
219
+ const { stdout } = await execAsync(`echo "${input}" | git credential fill`, {
220
+ timeout: 5000,
221
+ });
222
+ // If we get username and password back, credentials exist
223
+ return stdout.includes('username=') && stdout.includes('password=');
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ }
229
+ /**
230
+ * Save git credentials using git credential store
231
+ */
232
+ async function saveGitCredentials(host, username, token) {
233
+ // First, ensure credential helper is configured
234
+ try {
235
+ const { stdout: helperOutput } = await execAsync('git config --global credential.helper');
236
+ if (!helperOutput.trim()) {
237
+ // Configure credential helper based on OS
238
+ const platform = process.platform;
239
+ if (platform === 'darwin') {
240
+ await execAsync('git config --global credential.helper osxkeychain');
241
+ }
242
+ else if (platform === 'win32') {
243
+ await execAsync('git config --global credential.helper wincred');
244
+ }
245
+ else {
246
+ await execAsync('git config --global credential.helper store');
247
+ }
248
+ }
249
+ }
250
+ catch {
251
+ // If no helper configured, set one
252
+ const platform = process.platform;
253
+ if (platform === 'darwin') {
254
+ await execAsync('git config --global credential.helper osxkeychain');
255
+ }
256
+ else if (platform === 'win32') {
257
+ await execAsync('git config --global credential.helper wincred');
258
+ }
259
+ else {
260
+ await execAsync('git config --global credential.helper store');
261
+ }
262
+ }
263
+ // Save credentials using git credential approve
264
+ const credentialInput = `protocol=https
265
+ host=${host}
266
+ username=${username}
267
+ password=${token}
268
+ `;
269
+ await execAsync(`printf "${credentialInput}" | git credential approve`);
270
+ }
271
+ /**
272
+ * Generate helpful authentication error message
273
+ */
274
+ function generateAuthErrorMessage(host, repoUrl) {
275
+ return `
276
+ ❌ **Authentication Failed**: Cannot access private repository \`${repoUrl}\`
277
+
278
+ This is a private GitLab repository that requires authentication.
279
+
280
+ ---
281
+
282
+ ### 🔐 First, check if credentials already exist
283
+
284
+ \`\`\`
285
+ check_gitlab_credentials:
286
+ host: ${host}
287
+ \`\`\`
288
+
289
+ ---
290
+
291
+ ### If no credentials, use \`setup_gitlab_credentials\` to login
292
+
293
+ Just enter your username and password once. The system will automatically obtain and save a Token:
294
+
295
+ \`\`\`
296
+ setup_gitlab_credentials:
297
+ host: ${host}
298
+ username: <your_gitlab_username>
299
+ password: <your_gitlab_password>
300
+ \`\`\`
301
+
302
+ 🔒 **Security Note:**
303
+ - Your password is **only used once for login** and is NOT stored
304
+ - Only the auto-generated Token is saved
305
+ - No need to enter password again in the future
306
+
307
+ **Note:** If Two-Factor Authentication (2FA) is enabled, you will be guided to create a Token manually.
308
+
309
+ ---
310
+
311
+ Once configured, future private skill installations will not require any credentials!
312
+ `.trim();
313
+ }
9
314
  // Configuration
10
315
  const DEFAULT_API_HOST = process.env.SKILLS_API_HOST || "";
11
316
  const DEFAULT_AUTH_TOKEN = process.env.SKILLS_AUTH_TOKEN || "";
@@ -189,11 +494,29 @@ async function installFromGitHub(urlInfo, destPath) {
189
494
  /**
190
495
  * Install Skill from GitLab using git sparse-checkout
191
496
  * For private GitLab repositories that degit doesn't support
497
+ * @param urlInfo - Parsed git URL information
498
+ * @param destPath - Destination path for the skill
499
+ * @param gitPassword - Optional GitLab password (Personal Access Token) for authentication
500
+ * @param gitUsername - Optional GitLab username for authentication
501
+ * @param saveCredentials - Whether to save credentials after successful authentication
192
502
  */
193
- async function installFromGitLab(urlInfo, destPath) {
503
+ async function installFromGitLab(urlInfo, destPath, gitPassword, gitUsername, saveCredentials = true) {
194
504
  const skillsDir = path.dirname(destPath);
195
505
  const tempDir = `temp-${urlInfo.skillName}-${Date.now()}`;
196
- const repoUrl = `https://${urlInfo.host}/${urlInfo.owner}/${urlInfo.repo}.git`;
506
+ // Build repo URL - if password provided, use authenticated URL
507
+ let repoUrl;
508
+ let displayUrl;
509
+ // Determine username: provided > system username > oauth2
510
+ const effectiveUsername = gitUsername || process.env.USER || process.env.USERNAME || "oauth2";
511
+ if (gitPassword) {
512
+ // Use username:password in URL for authentication
513
+ repoUrl = `https://${encodeURIComponent(effectiveUsername)}:${encodeURIComponent(gitPassword)}@${urlInfo.host}/${urlInfo.owner}/${urlInfo.repo}.git`;
514
+ displayUrl = `https://${urlInfo.host}/${urlInfo.owner}/${urlInfo.repo}.git`;
515
+ }
516
+ else {
517
+ repoUrl = `https://${urlInfo.host}/${urlInfo.owner}/${urlInfo.repo}.git`;
518
+ displayUrl = repoUrl;
519
+ }
197
520
  // Build command sequence:
198
521
  // 1. Create target directory
199
522
  // 2. Clone with sparse checkout
@@ -210,14 +533,46 @@ async function installFromGitLab(urlInfo, destPath) {
210
533
  `mv ${tempDir}/${urlInfo.skillPath} ${urlInfo.skillName}`,
211
534
  `rm -rf ${tempDir}`,
212
535
  ].join(" && ");
213
- const { stdout, stderr } = await execAsync(commands);
214
- const output = stdout || stderr || "Installation completed";
215
- return `✅ Skill "${urlInfo.skillName}" has been successfully installed to ${destPath}\n\nSource: ${repoUrl} (${urlInfo.skillPath})\n${output}`;
536
+ try {
537
+ const { stdout, stderr } = await execAsync(commands);
538
+ const output = stdout || stderr || "Installation completed";
539
+ // If we used a password and saveCredentials is true, save credentials for future use
540
+ if (gitPassword && saveCredentials) {
541
+ try {
542
+ await saveGitCredentials(urlInfo.host, effectiveUsername, gitPassword);
543
+ }
544
+ catch {
545
+ // Silently fail credential saving - installation was successful
546
+ }
547
+ }
548
+ return `✅ Skill "${urlInfo.skillName}" has been successfully installed to ${destPath}\n\nSource: ${displayUrl} (${urlInfo.skillPath})\n${output}`;
549
+ }
550
+ catch (error) {
551
+ const errorMessage = error instanceof Error ? error.message : String(error);
552
+ // Check if this is an authentication error
553
+ if (isAuthenticationError(errorMessage)) {
554
+ // Clean up any partial temp directory
555
+ try {
556
+ await execAsync(`rm -rf ${path.join(skillsDir, tempDir)}`);
557
+ }
558
+ catch {
559
+ // Ignore cleanup errors
560
+ }
561
+ throw new Error(generateAuthErrorMessage(urlInfo.host, displayUrl));
562
+ }
563
+ throw error;
564
+ }
216
565
  }
217
566
  /**
218
567
  * Install Skill to specified project using gitUrl
568
+ * @param gitUrl - Git URL of the skill
569
+ * @param projectPath - Path to the project where skill will be installed
570
+ * @param targetDir - Target directory within the project (default: .claude/skills)
571
+ * @param gitPassword - Optional GitLab password (Personal Access Token) for private repos
572
+ * @param gitUsername - Optional GitLab username for authentication
573
+ * @param saveCredentials - Whether to save credentials after successful auth
219
574
  */
220
- async function installSkillFromGitUrl(gitUrl, projectPath, targetDir = ".claude/skills") {
575
+ async function installSkillFromGitUrl(gitUrl, projectPath, targetDir = ".claude/skills", gitPassword, gitUsername, saveCredentials = true) {
221
576
  const urlInfo = parseGitUrl(gitUrl);
222
577
  if (!urlInfo) {
223
578
  throw new Error(`Could not parse git URL: ${gitUrl}`);
@@ -231,7 +586,7 @@ async function installSkillFromGitUrl(gitUrl, projectPath, targetDir = ".claude/
231
586
  }
232
587
  else if (urlInfo.type === "gitlab") {
233
588
  // Use git sparse-checkout for GitLab (supports private repos)
234
- return await installFromGitLab(urlInfo, destPath);
589
+ return await installFromGitLab(urlInfo, destPath, gitPassword, gitUsername, saveCredentials);
235
590
  }
236
591
  else {
237
592
  throw new Error(`Unsupported git URL type: ${gitUrl}`);
@@ -344,7 +699,7 @@ server.tool("list_skills", "Fetch company Skills list. You can search for specif
344
699
  }
345
700
  });
346
701
  // Register tool: Install Skill
347
- server.tool("install_skill", "Install skill to local project by gitUrl. Uses npx degit for GitHub repositories and git sparse-checkout for GitLab repositories. Project path is required, which can be the project path of the currently opened file in IDE.", {
702
+ server.tool("install_skill", "Install skill to local project by gitUrl. Uses npx degit for GitHub repositories and git sparse-checkout for GitLab repositories. Project path is required, which can be the project path of the currently opened file in IDE. For private GitLab repositories, provide gitToken for authentication.", {
348
703
  gitUrl: z.string().describe("Git URL of the skill (e.g., 'https://github.com/anthropics/skills/tree/main/skills/webapp-testing'). The skill name will be extracted from this URL."),
349
704
  projectPath: z
350
705
  .string()
@@ -354,7 +709,19 @@ server.tool("install_skill", "Install skill to local project by gitUrl. Uses npx
354
709
  .string()
355
710
  .optional()
356
711
  .describe("Installation target directory, default: .claude/skills"),
357
- }, async ({ gitUrl, projectPath, targetDir }) => {
712
+ gitPassword: z
713
+ .string()
714
+ .optional()
715
+ .describe("GitLab password (Personal Access Token) for private repository authentication. If provided, will be used for this installation and saved for future use."),
716
+ gitUsername: z
717
+ .string()
718
+ .optional()
719
+ .describe("GitLab username for authentication. Default: your system username or 'oauth2'"),
720
+ saveCredentials: z
721
+ .boolean()
722
+ .optional()
723
+ .describe("Whether to save the provided credentials for future use. Default: true"),
724
+ }, async ({ gitUrl, projectPath, targetDir, gitPassword, gitUsername, saveCredentials }) => {
358
725
  // Check if project path is provided
359
726
  if (!projectPath) {
360
727
  return {
@@ -368,12 +735,17 @@ server.tool("install_skill", "Install skill to local project by gitUrl. Uses npx
368
735
  };
369
736
  }
370
737
  try {
371
- const result = await installSkillFromGitUrl(gitUrl, projectPath, targetDir || ".claude/skills");
738
+ const result = await installSkillFromGitUrl(gitUrl, projectPath, targetDir || ".claude/skills", gitPassword, gitUsername, saveCredentials !== false // default to true
739
+ );
740
+ let successMessage = result;
741
+ if (gitPassword && saveCredentials !== false) {
742
+ successMessage += "\n\n💾 **Credentials Saved**: GitLab credentials have been automatically saved to your system. No need to provide password for future installations.";
743
+ }
372
744
  return {
373
745
  content: [
374
746
  {
375
747
  type: "text",
376
- text: result,
748
+ text: successMessage,
377
749
  },
378
750
  ],
379
751
  };
@@ -383,7 +755,7 @@ server.tool("install_skill", "Install skill to local project by gitUrl. Uses npx
383
755
  content: [
384
756
  {
385
757
  type: "text",
386
- text: `❌ Error: ${error instanceof Error ? error.message : "Unknown error"}`,
758
+ text: `${error instanceof Error ? error.message : "Unknown error"}`,
387
759
  },
388
760
  ],
389
761
  isError: true,
@@ -391,7 +763,7 @@ server.tool("install_skill", "Install skill to local project by gitUrl. Uses npx
391
763
  }
392
764
  });
393
765
  // Register tool: Install Skill Group
394
- server.tool("install_skill_group", "Install ALL skills from a skill group to local project at once. When user wants to install an entire skill group (shown in list_skills results with group ID), use this tool instead of install_skill. This will fetch the group details by ID and install all skills that have gitUrl.", {
766
+ server.tool("install_skill_group", "Install ALL skills from a skill group to local project at once. When user wants to install an entire skill group (shown in list_skills results with group ID), use this tool instead of install_skill. This will fetch the group details by ID and install all skills that have gitUrl. For private GitLab repositories, provide gitToken for authentication.", {
395
767
  groupId: z.string().describe("The ID of the skill group to install. You can get this ID from list_skills results (e.g., '697190fc88f4e586fb1a7114')"),
396
768
  host: z
397
769
  .string()
@@ -409,7 +781,15 @@ server.tool("install_skill_group", "Install ALL skills from a skill group to loc
409
781
  .string()
410
782
  .optional()
411
783
  .describe("Installation target directory, default: .claude/skills"),
412
- }, async ({ groupId, host, token, projectPath, targetDir }) => {
784
+ gitPassword: z
785
+ .string()
786
+ .optional()
787
+ .describe("GitLab password (Personal Access Token) for private repository authentication. If provided, will be used for all skill installations and saved for future use."),
788
+ gitUsername: z
789
+ .string()
790
+ .optional()
791
+ .describe("GitLab username for authentication. Default: your system username or 'oauth2'"),
792
+ }, async ({ groupId, host, token, projectPath, targetDir, gitPassword, gitUsername }) => {
413
793
  // Check if project path is provided
414
794
  if (!projectPath) {
415
795
  return {
@@ -468,10 +848,15 @@ server.tool("install_skill_group", "Install ALL skills from a skill group to loc
468
848
  const results = [];
469
849
  const errors = [];
470
850
  const installDir = targetDir || ".claude/skills";
851
+ let credentialsSaved = false;
471
852
  for (const skill of skillsWithGitUrl) {
472
853
  try {
473
- const result = await installSkillFromGitUrl(skill.gitUrl, projectPath, installDir);
854
+ const result = await installSkillFromGitUrl(skill.gitUrl, projectPath, installDir, gitPassword, gitUsername, !credentialsSaved // Only save credentials on first successful installation
855
+ );
474
856
  results.push(`✅ ${skill.title}: Installed successfully`);
857
+ if (gitPassword && !credentialsSaved) {
858
+ credentialsSaved = true;
859
+ }
475
860
  }
476
861
  catch (error) {
477
862
  const errorMsg = error instanceof Error ? error.message : "Unknown error";
@@ -479,7 +864,7 @@ server.tool("install_skill_group", "Install ALL skills from a skill group to loc
479
864
  }
480
865
  }
481
866
  // Build summary
482
- const summary = [
867
+ const summaryParts = [
483
868
  `## ${groupIcon} Skill Group: ${groupName}`,
484
869
  ``,
485
870
  `**Total skills:** ${skillsWithGitUrl.length}`,
@@ -489,7 +874,11 @@ server.tool("install_skill_group", "Install ALL skills from a skill group to loc
489
874
  `### Installation Results:`,
490
875
  ...results,
491
876
  ...(errors.length > 0 ? [``, `### Errors:`, ...errors] : []),
492
- ].join("\n");
877
+ ];
878
+ if (gitPassword && credentialsSaved) {
879
+ summaryParts.push(``, `💾 **Credentials Saved**: GitLab credentials have been automatically saved to your system. No need to provide password for future installations.`);
880
+ }
881
+ const summary = summaryParts.join("\n");
493
882
  return {
494
883
  content: [
495
884
  {
@@ -511,6 +900,230 @@ server.tool("install_skill_group", "Install ALL skills from a skill group to loc
511
900
  };
512
901
  }
513
902
  });
903
+ // Register tool: Check GitLab Credentials Status
904
+ server.tool("check_gitlab_credentials", "Check if GitLab credentials are already configured for a host. Use this to verify your credential status before trying to install private skills.", {
905
+ host: z
906
+ .string()
907
+ .describe("GitLab host domain to check (e.g., 'git.ringcentral.com')"),
908
+ }, async ({ host }) => {
909
+ try {
910
+ const hasCredentials = await checkGitCredentials(host);
911
+ if (hasCredentials) {
912
+ return {
913
+ content: [
914
+ {
915
+ type: "text",
916
+ text: `✅ **Credentials Configured!**
917
+
918
+ **Host:** \`${host}\`
919
+
920
+ Your GitLab credentials already exist. You can directly use:
921
+ - \`install_skill\` to install skills from private repositories
922
+ - \`install_skill_group\` to install private skill groups
923
+
924
+ **No need to configure credentials again!**`,
925
+ },
926
+ ],
927
+ };
928
+ }
929
+ else {
930
+ return {
931
+ content: [
932
+ {
933
+ type: "text",
934
+ text: `⚠️ **No Credentials Found**
935
+
936
+ **Host:** \`${host}\`
937
+
938
+ GitLab credentials for this host have not been configured. Please use \`setup_gitlab_credentials\` to configure:
939
+
940
+ \`\`\`
941
+ setup_gitlab_credentials:
942
+ host: ${host}
943
+ username: <your_gitlab_username>
944
+ password: <your_gitlab_password>
945
+ \`\`\`
946
+
947
+ **Security Note:** Your password is only used once for login to obtain a Token and is NOT stored.`,
948
+ },
949
+ ],
950
+ };
951
+ }
952
+ }
953
+ catch (error) {
954
+ return {
955
+ content: [
956
+ {
957
+ type: "text",
958
+ text: `❌ Error checking credentials: ${error instanceof Error ? error.message : "Unknown error"}`,
959
+ },
960
+ ],
961
+ isError: true,
962
+ };
963
+ }
964
+ });
965
+ // Register tool: Setup GitLab Credentials (with auto-login)
966
+ server.tool("setup_gitlab_credentials", "Login to GitLab with your username and password to automatically configure credentials. Your password is ONLY used once to obtain a Token and is NOT stored anywhere. The Token will be saved for future use.", {
967
+ host: z
968
+ .string()
969
+ .describe("GitLab host domain (e.g., 'git.ringcentral.com', 'gitlab.com')"),
970
+ username: z
971
+ .string()
972
+ .describe("Your GitLab username (e.g., 'john.doe')"),
973
+ password: z
974
+ .string()
975
+ .describe("Your GitLab password (used ONCE to login and get Token, NOT stored)"),
976
+ force: z
977
+ .boolean()
978
+ .optional()
979
+ .describe("Force re-login even if credentials already exist. Default: false"),
980
+ openBrowser: z
981
+ .boolean()
982
+ .optional()
983
+ .describe("If auto-login fails, whether to open browser to manually create token. Default: true"),
984
+ }, async ({ host, username, password, force, openBrowser }) => {
985
+ try {
986
+ // Step 0: Check if credentials already exist (unless force=true)
987
+ if (!force) {
988
+ const existingCredentials = await checkGitCredentials(host);
989
+ if (existingCredentials) {
990
+ return {
991
+ content: [
992
+ {
993
+ type: "text",
994
+ text: `✅ **Credentials Already Exist, No Need to Reconfigure!**
995
+
996
+ **Host:** \`${host}\`
997
+
998
+ Your GitLab credentials are already configured. You can directly use \`install_skill\` to install private skills.
999
+
1000
+ If you need to re-login (e.g., Token expired), add \`force: true\` parameter:
1001
+
1002
+ \`\`\`
1003
+ setup_gitlab_credentials:
1004
+ host: ${host}
1005
+ username: ${username}
1006
+ password: <your_password>
1007
+ force: true
1008
+ \`\`\``,
1009
+ },
1010
+ ],
1011
+ };
1012
+ }
1013
+ }
1014
+ // Step 1: Try auto-login to GitLab and create token
1015
+ const loginResult = await loginToGitLabAndCreateToken(host, username, password);
1016
+ if (loginResult.success && loginResult.token) {
1017
+ // Auto-login succeeded! Save the token (NOT password) as git credentials
1018
+ await saveGitCredentials(host, username, loginResult.token);
1019
+ return {
1020
+ content: [
1021
+ {
1022
+ type: "text",
1023
+ text: `✅ **GitLab Login Successful! Credentials Auto-Configured!**
1024
+
1025
+ **Host:** \`${host}\`
1026
+ **Username:** \`${username}\`
1027
+ **Token:** Auto-created and saved (valid for 1 year)
1028
+
1029
+ 🔒 **Security Note:**
1030
+ - Your **password was NOT saved**, only used for this one-time login
1031
+ - Only the auto-generated **Token** is stored (not your password)
1032
+ - Token is stored in your system's Git credential manager (e.g., macOS Keychain)
1033
+
1034
+ Now you can:
1035
+ - Use \`install_skill\` to install skills from private repositories
1036
+ - Use \`install_skill_group\` to install private skill groups
1037
+
1038
+ **No need to enter any credentials in the future!**`,
1039
+ },
1040
+ ],
1041
+ };
1042
+ }
1043
+ // Auto-login failed, provide guidance
1044
+ const errorMessage = loginResult.error || "Unknown error";
1045
+ // If openBrowser is not explicitly false, try to open browser
1046
+ if (openBrowser !== false) {
1047
+ try {
1048
+ const tokenUrl = `https://${host}/-/user_settings/personal_access_tokens`;
1049
+ await openInBrowser(tokenUrl);
1050
+ return {
1051
+ content: [
1052
+ {
1053
+ type: "text",
1054
+ text: `⚠️ **Auto-login Failed**: ${errorMessage}
1055
+
1056
+ Browser has been opened to the Token creation page. Please follow these steps:
1057
+
1058
+ 1. Login to GitLab in the browser (if not already logged in)
1059
+ 2. Enter a name in "Token name" (e.g., \`ring-skills-mcp\`)
1060
+ 3. Select an expiration date in "Expiration date" (recommended: 1 year)
1061
+ 4. Check \`read_repository\` scope
1062
+ 5. Click "Create personal access token"
1063
+ 6. **Copy the generated Token**
1064
+
1065
+ Then call this tool again, paste the Token in the password field:
1066
+
1067
+ \`\`\`
1068
+ setup_gitlab_credentials:
1069
+ host: ${host}
1070
+ username: ${username}
1071
+ password: <paste_your_copied_token>
1072
+ \`\`\``,
1073
+ },
1074
+ ],
1075
+ };
1076
+ }
1077
+ catch {
1078
+ // Failed to open browser, fall through to manual instructions
1079
+ }
1080
+ }
1081
+ return {
1082
+ content: [
1083
+ {
1084
+ type: "text",
1085
+ text: `❌ **Auto-login Failed**: ${errorMessage}
1086
+
1087
+ **Manual Token Creation Steps:**
1088
+
1089
+ 1. Open browser and visit: https://${host}/-/user_settings/personal_access_tokens
1090
+ 2. Login to your GitLab account
1091
+ 3. Create a new Token:
1092
+ - Token name: \`ring-skills-mcp\`
1093
+ - Expiration: Select a date 1 year from now
1094
+ - Scopes: Check \`read_repository\`
1095
+ 4. Click "Create personal access token"
1096
+ 5. Copy the generated Token
1097
+
1098
+ Then call this tool again, paste the Token in the password field.
1099
+
1100
+ **Common Issues:**
1101
+ - If your account has Two-Factor Authentication (2FA) enabled, you must create a Token manually
1102
+ - If you forgot your password, please reset it on the GitLab website first`,
1103
+ },
1104
+ ],
1105
+ isError: true,
1106
+ };
1107
+ }
1108
+ catch (error) {
1109
+ return {
1110
+ content: [
1111
+ {
1112
+ type: "text",
1113
+ text: `❌ **Login Process Error**
1114
+
1115
+ Error: ${error instanceof Error ? error.message : "Unknown error"}
1116
+
1117
+ Please create a Token manually:
1118
+ 1. Open https://${host}/-/user_settings/personal_access_tokens
1119
+ 2. Create a Token with \`read_repository\` scope
1120
+ 3. Call this tool again, paste the Token in the password field`,
1121
+ },
1122
+ ],
1123
+ isError: true,
1124
+ };
1125
+ }
1126
+ });
514
1127
  // Start server
515
1128
  async function main() {
516
1129
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ring-skills-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "MCP service for fetching and installing company Skills",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",