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.
Files changed (2) hide show
  1. package/index.js +69 -27
  2. 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.30'; // Add warnings for slow-starting services (Cassandra, MongoDB, etc)
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
- const publicKey = await fs.readFile(`${keyPath}.pub`, 'utf8');
212
- return { publicKey: publicKey.trim(), privateKeyPath: keyPath };
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
- const { stdout, stderr } = await execAsync(
218
- `ssh-keygen -t ed25519 -f "${keyPath}" -N "" -C "${email}"`,
219
- { timeout: 10000 }
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
- if (stderr && !stderr.includes('Generating public/private')) {
223
- throw new Error(`SSH key generation failed: ${stderr}`);
224
- }
227
+ if (stderr && !stderr.includes('Generating public/private')) {
228
+ throw new Error(`SSH key generation failed: ${stderr}`);
229
+ }
225
230
 
226
- await execAsync(`chmod 600 "${keyPath}"`);
227
- await execAsync(`chmod 644 "${keyPath}.pub"`);
231
+ await execAsync(`chmod 600 "${keyPath}"`);
232
+ await execAsync(`chmod 644 "${keyPath}.pub"`);
228
233
 
229
- const publicKey = await fs.readFile(`${keyPath}.pub`, 'utf8');
234
+ publicKey = await fs.readFile(`${keyPath}.pub`, 'utf8');
235
+ }
230
236
 
231
- // Add to SSH config
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
- if (existingConfig.includes('Host git.mlgym.io')) {
245
- // Replace existing MLGym SSH config block with new key
246
- const updatedConfig = existingConfig.replace(
247
- /# MLGym GitLab.*?Host git\.mlgym\.io.*?(?=\n#|\n\nHost|\n?$)/gs,
248
- ''
249
- ).trim();
250
- await fs.writeFile(configPath, updatedConfig + configEntry, { mode: 0o600 });
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 - will be used as subdomain (e.g., "myapp" for myapp.ezb.net)'
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: 'Deployment hostname/subdomain (optional, defaults to project_name). Will be accessible at https://<hostname>.ezb.net',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlgym-deploy",
3
- "version": "3.3.31",
3
+ "version": "3.3.34",
4
4
  "description": "MCP server for MLGym - Complete deployment management: deploy, configure, monitor, and rollback applications",
5
5
  "main": "index.js",
6
6
  "type": "module",