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.
- package/dist/index.js +630 -17
- 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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
]
|
|
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();
|