mlgym-deploy 3.3.31 → 3.3.34
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/index.js +69 -27
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import crypto from 'crypto';
|
|
|
18
18
|
const execAsync = promisify(exec);
|
|
19
19
|
|
|
20
20
|
// Current version of this MCP server - INCREMENT FOR WORKFLOW FIXES
|
|
21
|
-
const CURRENT_VERSION = '3.3.
|
|
21
|
+
const CURRENT_VERSION = '3.3.34'; // Fix duplicate project creation + SSH key config always updates for current user
|
|
22
22
|
const PACKAGE_NAME = 'mlgym-deploy';
|
|
23
23
|
|
|
24
24
|
// Debug logging configuration - ENABLED BY DEFAULT
|
|
@@ -205,33 +205,40 @@ async function generateSSHKeyPair(email) {
|
|
|
205
205
|
const sanitizedEmail = email.replace('@', '_at_').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
206
206
|
const keyPath = path.join(sshDir, `mlgym_${sanitizedEmail}`);
|
|
207
207
|
|
|
208
|
+
let keyExists = false;
|
|
209
|
+
let publicKey = '';
|
|
210
|
+
|
|
208
211
|
try {
|
|
209
212
|
await fs.access(keyPath);
|
|
210
213
|
console.error(`SSH key already exists at ${keyPath}, using existing key`);
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
publicKey = await fs.readFile(`${keyPath}.pub`, 'utf8');
|
|
215
|
+
keyExists = true;
|
|
213
216
|
} catch {
|
|
214
217
|
// Key doesn't exist, generate new one
|
|
218
|
+
console.error(`Generating new SSH key for ${email}...`);
|
|
215
219
|
}
|
|
216
220
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
if (!keyExists) {
|
|
222
|
+
const { stdout, stderr } = await execAsync(
|
|
223
|
+
`ssh-keygen -t ed25519 -f "${keyPath}" -N "" -C "${email}"`,
|
|
224
|
+
{ timeout: 10000 }
|
|
225
|
+
);
|
|
221
226
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
227
|
+
if (stderr && !stderr.includes('Generating public/private')) {
|
|
228
|
+
throw new Error(`SSH key generation failed: ${stderr}`);
|
|
229
|
+
}
|
|
225
230
|
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
await execAsync(`chmod 600 "${keyPath}"`);
|
|
232
|
+
await execAsync(`chmod 644 "${keyPath}.pub"`);
|
|
228
233
|
|
|
229
|
-
|
|
234
|
+
publicKey = await fs.readFile(`${keyPath}.pub`, 'utf8');
|
|
235
|
+
}
|
|
230
236
|
|
|
231
|
-
//
|
|
237
|
+
// FIX: ALWAYS update SSH config to point to THIS user's key
|
|
238
|
+
// This prevents the bug where User A's key remains in config after User B authenticates
|
|
232
239
|
const configPath = path.join(sshDir, 'config');
|
|
233
240
|
const configEntry = `
|
|
234
|
-
# MLGym GitLab (added by mlgym-deploy)
|
|
241
|
+
# MLGym GitLab (added by mlgym-deploy for ${email})
|
|
235
242
|
Host git.mlgym.io
|
|
236
243
|
User git
|
|
237
244
|
Port 22
|
|
@@ -241,18 +248,16 @@ Host git.mlgym.io
|
|
|
241
248
|
|
|
242
249
|
try {
|
|
243
250
|
const existingConfig = await fs.readFile(configPath, 'utf8');
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
} else {
|
|
252
|
-
await fs.appendFile(configPath, configEntry);
|
|
253
|
-
}
|
|
251
|
+
// Always remove old MLGym config and add new one for current user
|
|
252
|
+
const updatedConfig = existingConfig.replace(
|
|
253
|
+
/\n?# MLGym GitLab[^\n]*\nHost git\.mlgym\.io\n(?:[ \t]+[^\n]+\n)*/g,
|
|
254
|
+
''
|
|
255
|
+
).trim();
|
|
256
|
+
await fs.writeFile(configPath, updatedConfig + '\n' + configEntry, { mode: 0o600 });
|
|
257
|
+
console.error(`SSH config updated to use key for ${email}`);
|
|
254
258
|
} catch {
|
|
255
259
|
await fs.writeFile(configPath, configEntry, { mode: 0o600 });
|
|
260
|
+
console.error(`SSH config created for ${email}`);
|
|
256
261
|
}
|
|
257
262
|
|
|
258
263
|
return { publicKey: publicKey.trim(), privateKeyPath: keyPath };
|
|
@@ -1660,7 +1665,7 @@ async function initProject(args) {
|
|
|
1660
1665
|
status: 'error',
|
|
1661
1666
|
message: 'Hostname is required when deployment is enabled',
|
|
1662
1667
|
required_fields: {
|
|
1663
|
-
hostname: '✗ missing -
|
|
1668
|
+
hostname: '✗ missing - project identifier (actual URL will be https://{app_id}.eu{1,2,3}.ezb.net)'
|
|
1664
1669
|
}
|
|
1665
1670
|
}, null, 2)
|
|
1666
1671
|
}]
|
|
@@ -1683,6 +1688,43 @@ async function initProject(args) {
|
|
|
1683
1688
|
|
|
1684
1689
|
console.error(`Creating project: ${name}`);
|
|
1685
1690
|
|
|
1691
|
+
// FIX: Check if project already exists on backend before creating (prevents duplicates)
|
|
1692
|
+
// This is critical to prevent race conditions where multiple concurrent requests
|
|
1693
|
+
// could create duplicate projects
|
|
1694
|
+
log.info(`MCP >>> [initProject] Checking if project '${name}' already exists on backend...`);
|
|
1695
|
+
const existingProjectsResult = await apiRequest('GET', '/api/v1/projects', null, true);
|
|
1696
|
+
if (existingProjectsResult.success && Array.isArray(existingProjectsResult.data)) {
|
|
1697
|
+
// Check if any existing project name ends with our project name (user prefix is added by backend)
|
|
1698
|
+
const existingProject = existingProjectsResult.data.find(p =>
|
|
1699
|
+
p.name && (p.name === name || p.name.endsWith(`-${name}`))
|
|
1700
|
+
);
|
|
1701
|
+
if (existingProject) {
|
|
1702
|
+
log.warning(`MCP >>> [initProject] Project '${name}' already exists (found: ${existingProject.name})`);
|
|
1703
|
+
return {
|
|
1704
|
+
content: [{
|
|
1705
|
+
type: 'text',
|
|
1706
|
+
text: JSON.stringify({
|
|
1707
|
+
status: 'already_exists',
|
|
1708
|
+
message: `Project '${name}' already exists on the server`,
|
|
1709
|
+
existing_project: {
|
|
1710
|
+
id: existingProject.id,
|
|
1711
|
+
name: existingProject.name,
|
|
1712
|
+
ssh_url: existingProject.ssh_url_to_repo,
|
|
1713
|
+
web_url: existingProject.web_url,
|
|
1714
|
+
deployment_url: existingProject.deployment_url
|
|
1715
|
+
},
|
|
1716
|
+
next_steps: [
|
|
1717
|
+
'Use the existing project instead of creating a new one',
|
|
1718
|
+
'To update: git push mlgym main',
|
|
1719
|
+
'To check status: use mlgym_status tool'
|
|
1720
|
+
]
|
|
1721
|
+
}, null, 2)
|
|
1722
|
+
}]
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
log.success(`MCP >>> [initProject] No existing project found, proceeding with creation`);
|
|
1727
|
+
|
|
1686
1728
|
// Create project via backend API with FLAT structure (matching CLI)
|
|
1687
1729
|
const projectData = {
|
|
1688
1730
|
name: name,
|
|
@@ -3699,7 +3741,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
3699
3741
|
},
|
|
3700
3742
|
hostname: {
|
|
3701
3743
|
type: 'string',
|
|
3702
|
-
description: '
|
|
3744
|
+
description: 'Custom hostname (optional). The actual deployment URL will be https://{app_id}.eu{1,2,3}.ezb.net based on the assigned server.',
|
|
3703
3745
|
pattern: '^[a-z][a-z0-9-]*[a-z0-9]$',
|
|
3704
3746
|
minLength: 3,
|
|
3705
3747
|
maxLength: 63
|
package/package.json
CHANGED