mlgym-deploy 2.1.0 → 2.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/index-v2.js +24 -2
- package/index.js +479 -26
- package/package.json +1 -1
package/index-v2.js
CHANGED
|
@@ -964,25 +964,44 @@ async function deployProject(sessionId, args) {
|
|
|
964
964
|
description: `Deployed via MLGym MCP from ${session.framework} project`,
|
|
965
965
|
visibility: 'private',
|
|
966
966
|
enable_deployment: true,
|
|
967
|
-
deployment_region: region,
|
|
968
967
|
webhook_secret: crypto.randomBytes(16).toString('hex')
|
|
968
|
+
// Note: deployment_region is not used by backend, it uses random server selection
|
|
969
969
|
};
|
|
970
970
|
|
|
971
|
+
console.error(`Creating project with deployment enabled for ${session.projectName} in region ${region}`);
|
|
971
972
|
const result = await apiRequest('POST', '/api/v1/projects', projectData);
|
|
972
973
|
|
|
973
974
|
if (!result.success) {
|
|
975
|
+
console.error(`Project creation failed: ${result.error}`);
|
|
976
|
+
// Provide more detailed error information
|
|
974
977
|
return {
|
|
975
978
|
content: [{
|
|
976
979
|
type: 'text',
|
|
977
980
|
text: JSON.stringify({
|
|
978
981
|
status: 'error',
|
|
979
|
-
message: `Deployment failed: ${result.error}
|
|
982
|
+
message: `Deployment failed: ${result.error}`,
|
|
983
|
+
details: {
|
|
984
|
+
project_name: session.projectName,
|
|
985
|
+
region: region,
|
|
986
|
+
framework: session.framework,
|
|
987
|
+
suggestion: 'Please ensure the backend and Coolify services are running properly'
|
|
988
|
+
}
|
|
980
989
|
}, null, 2)
|
|
981
990
|
}]
|
|
982
991
|
};
|
|
983
992
|
}
|
|
984
993
|
|
|
985
994
|
const project = result.data;
|
|
995
|
+
console.error(`Project created successfully: ${project.name} (ID: ${project.id})`);
|
|
996
|
+
console.error(`SSH URL: ${project.ssh_url_to_repo}`);
|
|
997
|
+
|
|
998
|
+
// Check if deployment was actually created
|
|
999
|
+
const deploymentCreated = project.deployment_status || project.coolify_resource_id;
|
|
1000
|
+
if (deploymentCreated) {
|
|
1001
|
+
console.error(`Deployment resource created in Coolify`);
|
|
1002
|
+
} else {
|
|
1003
|
+
console.error(`Warning: Project created but deployment resource might not be set up`);
|
|
1004
|
+
}
|
|
986
1005
|
|
|
987
1006
|
// Initialize git repository
|
|
988
1007
|
const gitCommands = [
|
|
@@ -990,6 +1009,8 @@ async function deployProject(sessionId, args) {
|
|
|
990
1009
|
`git remote add origin ${project.ssh_url_to_repo}`,
|
|
991
1010
|
'git add .',
|
|
992
1011
|
'git commit -m "Initial deployment via MLGym"',
|
|
1012
|
+
'# Wait 5-10 seconds for SSH key to propagate in GitLab',
|
|
1013
|
+
'sleep 10',
|
|
993
1014
|
'git push -u origin main'
|
|
994
1015
|
];
|
|
995
1016
|
|
|
@@ -1014,6 +1035,7 @@ async function deployProject(sessionId, args) {
|
|
|
1014
1035
|
ssl: 'auto-provisioned',
|
|
1015
1036
|
cdn: 'enabled'
|
|
1016
1037
|
},
|
|
1038
|
+
important_note: '⚠️ SSH key propagation: Please wait 10 seconds before pushing to allow GitLab to activate your SSH key',
|
|
1017
1039
|
next_steps: {
|
|
1018
1040
|
message: 'Run these commands in your project directory:',
|
|
1019
1041
|
commands: gitCommands
|
package/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import crypto from 'crypto';
|
|
|
17
17
|
const execAsync = promisify(exec);
|
|
18
18
|
|
|
19
19
|
// Current version of this MCP server
|
|
20
|
-
const CURRENT_VERSION = '2.
|
|
20
|
+
const CURRENT_VERSION = '2.3.0';
|
|
21
21
|
const PACKAGE_NAME = 'mlgym-deploy';
|
|
22
22
|
|
|
23
23
|
// Version check state
|
|
@@ -49,18 +49,7 @@ async function checkForUpdates() {
|
|
|
49
49
|
|
|
50
50
|
lastVersionCheck = Date.now();
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
if (isUpdateAvailable) {
|
|
54
|
-
console.error(`\n╔════════════════════════════════════════════════════════════════╗`);
|
|
55
|
-
console.error(`║ MLGym MCP Server Update Available! ║`);
|
|
56
|
-
console.error(`║ ║`);
|
|
57
|
-
console.error(`║ Current version: ${CURRENT_VERSION.padEnd(49)} ║`);
|
|
58
|
-
console.error(`║ Latest version: ${latestVersion.padEnd(49)} ║`);
|
|
59
|
-
console.error(`║ ║`);
|
|
60
|
-
console.error(`║ To update, run: npm install -g ${PACKAGE_NAME}@latest ║`);
|
|
61
|
-
console.error(`╚════════════════════════════════════════════════════════════════╝\n`);
|
|
62
|
-
}
|
|
63
|
-
|
|
52
|
+
// Don't log here since we'll block execution in main()
|
|
64
53
|
return versionCheckResult;
|
|
65
54
|
} catch (error) {
|
|
66
55
|
// Silent fail - don't disrupt normal operation
|
|
@@ -411,9 +400,29 @@ const TOOLS = [
|
|
|
411
400
|
}
|
|
412
401
|
}
|
|
413
402
|
},
|
|
403
|
+
{
|
|
404
|
+
name: 'mlgym_auth_check',
|
|
405
|
+
description: 'Check if a user account exists and whether SSH keys are configured. Use this BEFORE creating a new account to avoid duplicates.',
|
|
406
|
+
inputSchema: {
|
|
407
|
+
type: 'object',
|
|
408
|
+
properties: {
|
|
409
|
+
email: {
|
|
410
|
+
type: 'string',
|
|
411
|
+
description: 'Email address to check',
|
|
412
|
+
pattern: '^[^@]+@[^@]+\\.[^@]+$'
|
|
413
|
+
},
|
|
414
|
+
password: {
|
|
415
|
+
type: 'string',
|
|
416
|
+
description: 'Password (optional - only needed if account exists)',
|
|
417
|
+
minLength: 8
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
required: ['email']
|
|
421
|
+
}
|
|
422
|
+
},
|
|
414
423
|
{
|
|
415
424
|
name: 'mlgym_user_create',
|
|
416
|
-
description: 'Create a new user
|
|
425
|
+
description: 'Create a new user OR setup SSH keys for existing user. First use mlgym_auth_check to verify if account exists.',
|
|
417
426
|
inputSchema: {
|
|
418
427
|
type: 'object',
|
|
419
428
|
properties: {
|
|
@@ -434,10 +443,15 @@ const TOOLS = [
|
|
|
434
443
|
},
|
|
435
444
|
accept_terms: {
|
|
436
445
|
type: 'boolean',
|
|
437
|
-
description: 'User must explicitly accept terms and conditions (REQUIRED
|
|
446
|
+
description: 'User must explicitly accept terms and conditions (REQUIRED for new users)'
|
|
447
|
+
},
|
|
448
|
+
setup_ssh_only: {
|
|
449
|
+
type: 'boolean',
|
|
450
|
+
description: 'Set to true to only setup SSH keys for existing user (skip user creation)',
|
|
451
|
+
default: false
|
|
438
452
|
}
|
|
439
453
|
},
|
|
440
|
-
required: ['email', '
|
|
454
|
+
required: ['email', 'password'] // Only email and password are always required
|
|
441
455
|
}
|
|
442
456
|
},
|
|
443
457
|
{
|
|
@@ -1181,6 +1195,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1181
1195
|
case 'mlgym_version_check':
|
|
1182
1196
|
return await versionCheck(args);
|
|
1183
1197
|
|
|
1198
|
+
case 'mlgym_auth_check':
|
|
1199
|
+
return await checkUserExists(args);
|
|
1200
|
+
|
|
1184
1201
|
case 'mlgym_user_create':
|
|
1185
1202
|
return await createUser(args);
|
|
1186
1203
|
|
|
@@ -1700,11 +1717,216 @@ async function configureGit(args) {
|
|
|
1700
1717
|
};
|
|
1701
1718
|
}
|
|
1702
1719
|
|
|
1703
|
-
// Tool implementation:
|
|
1720
|
+
// Tool implementation: Check if User Exists
|
|
1721
|
+
async function checkUserExists(args) {
|
|
1722
|
+
const { email, password } = args;
|
|
1723
|
+
|
|
1724
|
+
if (!email) {
|
|
1725
|
+
return {
|
|
1726
|
+
content: [{
|
|
1727
|
+
type: 'text',
|
|
1728
|
+
text: JSON.stringify({
|
|
1729
|
+
status: 'error',
|
|
1730
|
+
message: 'Email is required'
|
|
1731
|
+
}, null, 2)
|
|
1732
|
+
}]
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
console.error(`Checking if user exists: ${email}`);
|
|
1737
|
+
|
|
1738
|
+
// If password provided, try to login
|
|
1739
|
+
if (password) {
|
|
1740
|
+
try {
|
|
1741
|
+
const loginResult = await apiRequest('POST', '/api/v1/auth/login', {
|
|
1742
|
+
email,
|
|
1743
|
+
password
|
|
1744
|
+
}, false);
|
|
1745
|
+
|
|
1746
|
+
if (loginResult.success) {
|
|
1747
|
+
// Save auth token
|
|
1748
|
+
if (loginResult.data.token) {
|
|
1749
|
+
await saveAuth(email, loginResult.data.token);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Check SSH keys
|
|
1753
|
+
const keysResult = await apiRequest('GET', '/api/v1/keys', null, true);
|
|
1754
|
+
const sshKeys = keysResult.success ? keysResult.data : [];
|
|
1755
|
+
|
|
1756
|
+
return {
|
|
1757
|
+
content: [{
|
|
1758
|
+
type: 'text',
|
|
1759
|
+
text: JSON.stringify({
|
|
1760
|
+
status: 'success',
|
|
1761
|
+
exists: true,
|
|
1762
|
+
authenticated: true,
|
|
1763
|
+
email: email,
|
|
1764
|
+
user_id: loginResult.data.user?.user_id,
|
|
1765
|
+
gitlab_username: loginResult.data.user?.gitlab_username,
|
|
1766
|
+
ssh_keys_count: sshKeys.length,
|
|
1767
|
+
has_ssh_keys: sshKeys.length > 0,
|
|
1768
|
+
ssh_keys: sshKeys.map(key => ({
|
|
1769
|
+
id: key.id,
|
|
1770
|
+
title: key.title,
|
|
1771
|
+
created_at: key.created_at
|
|
1772
|
+
})),
|
|
1773
|
+
message: 'User exists and authenticated successfully'
|
|
1774
|
+
}, null, 2)
|
|
1775
|
+
}]
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
// Login failed, but user might exist
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Try to check if email exists without password (will fail but gives info)
|
|
1784
|
+
try {
|
|
1785
|
+
const result = await apiRequest('POST', '/api/v1/auth/login', {
|
|
1786
|
+
email,
|
|
1787
|
+
password: 'dummy_check'
|
|
1788
|
+
}, false);
|
|
1789
|
+
|
|
1790
|
+
// If we get here, it means the endpoint is accessible
|
|
1791
|
+
// The error message will tell us if user exists
|
|
1792
|
+
if (result.error && result.error.includes('Invalid credentials')) {
|
|
1793
|
+
return {
|
|
1794
|
+
content: [{
|
|
1795
|
+
type: 'text',
|
|
1796
|
+
text: JSON.stringify({
|
|
1797
|
+
status: 'success',
|
|
1798
|
+
exists: true,
|
|
1799
|
+
authenticated: false,
|
|
1800
|
+
email: email,
|
|
1801
|
+
message: 'User exists but needs password to authenticate',
|
|
1802
|
+
next_step: 'Please provide password to check SSH keys'
|
|
1803
|
+
}, null, 2)
|
|
1804
|
+
}]
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
// Network or other error
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// User doesn't exist
|
|
1812
|
+
return {
|
|
1813
|
+
content: [{
|
|
1814
|
+
type: 'text',
|
|
1815
|
+
text: JSON.stringify({
|
|
1816
|
+
status: 'success',
|
|
1817
|
+
exists: false,
|
|
1818
|
+
email: email,
|
|
1819
|
+
message: 'User does not exist. You can create a new account.',
|
|
1820
|
+
next_step: 'Use mlgym_user_create to create a new account'
|
|
1821
|
+
}, null, 2)
|
|
1822
|
+
}]
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Tool implementation: Create User or Setup SSH Keys
|
|
1704
1827
|
async function createUser(args) {
|
|
1705
|
-
let { email, name, password, accept_terms } = args;
|
|
1828
|
+
let { email, name, password, accept_terms, setup_ssh_only = false } = args;
|
|
1829
|
+
|
|
1830
|
+
// Check if this is SSH setup for existing user
|
|
1831
|
+
if (setup_ssh_only) {
|
|
1832
|
+
if (!email || !password) {
|
|
1833
|
+
return {
|
|
1834
|
+
content: [{
|
|
1835
|
+
type: 'text',
|
|
1836
|
+
text: JSON.stringify({
|
|
1837
|
+
status: 'error',
|
|
1838
|
+
message: 'Email and password required for SSH key setup',
|
|
1839
|
+
required_fields: {
|
|
1840
|
+
email: email ? '✓ provided' : '✗ missing',
|
|
1841
|
+
password: password ? '✓ provided' : '✗ missing'
|
|
1842
|
+
}
|
|
1843
|
+
}, null, 2)
|
|
1844
|
+
}]
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
console.error(`Setting up SSH keys for existing user: ${email}`);
|
|
1706
1849
|
|
|
1707
|
-
|
|
1850
|
+
// Login first
|
|
1851
|
+
try {
|
|
1852
|
+
const loginResult = await apiRequest('POST', '/api/v1/auth/login', {
|
|
1853
|
+
email,
|
|
1854
|
+
password
|
|
1855
|
+
}, false);
|
|
1856
|
+
|
|
1857
|
+
if (!loginResult.success) {
|
|
1858
|
+
return {
|
|
1859
|
+
content: [{
|
|
1860
|
+
type: 'text',
|
|
1861
|
+
text: `Failed to authenticate: ${loginResult.error}`
|
|
1862
|
+
}]
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// Save auth token
|
|
1867
|
+
if (loginResult.data.token) {
|
|
1868
|
+
await saveAuth(email, loginResult.data.token);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Generate SSH key
|
|
1872
|
+
console.error('Generating SSH key pair...');
|
|
1873
|
+
const { publicKey, privateKeyPath } = await generateSSHKeyPair(email);
|
|
1874
|
+
const publicKeyPath = privateKeyPath + '.pub';
|
|
1875
|
+
console.error(`SSH key generated: ${privateKeyPath}`);
|
|
1876
|
+
|
|
1877
|
+
// Add SSH key
|
|
1878
|
+
const keyTitle = `mlgym-${new Date().toISOString().split('T')[0]}`;
|
|
1879
|
+
const keyResult = await apiRequest('POST', '/api/v1/keys', {
|
|
1880
|
+
title: keyTitle,
|
|
1881
|
+
key: publicKey
|
|
1882
|
+
}, true);
|
|
1883
|
+
|
|
1884
|
+
if (keyResult.success) {
|
|
1885
|
+
return {
|
|
1886
|
+
content: [{
|
|
1887
|
+
type: 'text',
|
|
1888
|
+
text: JSON.stringify({
|
|
1889
|
+
status: 'success',
|
|
1890
|
+
message: 'SSH key added successfully',
|
|
1891
|
+
email: email,
|
|
1892
|
+
ssh_key_path: privateKeyPath,
|
|
1893
|
+
ssh_key_id: keyResult.data.id,
|
|
1894
|
+
next_steps: [
|
|
1895
|
+
`SSH key generated at: ${privateKeyPath}`,
|
|
1896
|
+
`Add to SSH agent: ssh-add "${privateKeyPath}"`,
|
|
1897
|
+
'Wait 10-15 seconds for SSH key to propagate to GitLab',
|
|
1898
|
+
'You can now push code to GitLab repositories'
|
|
1899
|
+
]
|
|
1900
|
+
}, null, 2)
|
|
1901
|
+
}]
|
|
1902
|
+
};
|
|
1903
|
+
} else {
|
|
1904
|
+
return {
|
|
1905
|
+
content: [{
|
|
1906
|
+
type: 'text',
|
|
1907
|
+
text: JSON.stringify({
|
|
1908
|
+
status: 'error',
|
|
1909
|
+
message: 'Failed to add SSH key',
|
|
1910
|
+
error: keyResult.error,
|
|
1911
|
+
manual_steps: [
|
|
1912
|
+
`SSH key generated at: ${publicKeyPath}`,
|
|
1913
|
+
'Manually add it at: https://git.mlgym.io/-/user_settings/ssh_keys'
|
|
1914
|
+
]
|
|
1915
|
+
}, null, 2)
|
|
1916
|
+
}]
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
} catch (err) {
|
|
1920
|
+
return {
|
|
1921
|
+
content: [{
|
|
1922
|
+
type: 'text',
|
|
1923
|
+
text: `Error setting up SSH keys: ${err.message}`
|
|
1924
|
+
}]
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// Regular user creation - validate all required fields
|
|
1708
1930
|
if (!email || !name || !password || accept_terms !== true) {
|
|
1709
1931
|
return {
|
|
1710
1932
|
content: [{
|
|
@@ -2019,10 +2241,47 @@ async function initProject(args) {
|
|
|
2019
2241
|
namespace: project.namespace?.path || project.namespace || auth.email.split('@')[0]
|
|
2020
2242
|
};
|
|
2021
2243
|
|
|
2022
|
-
// If deployment was enabled,
|
|
2244
|
+
// If deployment was enabled, create Dockerfile if not exists
|
|
2023
2245
|
if (enable_deployment) {
|
|
2024
2246
|
console.error('Deployment enabled - backend has set up webhook and Coolify resources automatically');
|
|
2025
2247
|
|
|
2248
|
+
// Check if Dockerfile already exists
|
|
2249
|
+
const dockerfilePath = path.join(local_path, 'Dockerfile');
|
|
2250
|
+
let dockerfileCreated = false;
|
|
2251
|
+
|
|
2252
|
+
try {
|
|
2253
|
+
await fs.access(dockerfilePath);
|
|
2254
|
+
console.error('Dockerfile already exists in project');
|
|
2255
|
+
} catch {
|
|
2256
|
+
// Dockerfile doesn't exist, create one based on detected framework
|
|
2257
|
+
console.error('No Dockerfile found, generating one...');
|
|
2258
|
+
|
|
2259
|
+
try {
|
|
2260
|
+
const framework = await detectFramework(local_path);
|
|
2261
|
+
console.error(`Detected framework: ${framework}`);
|
|
2262
|
+
|
|
2263
|
+
const dockerfileContent = generateDockerfile(framework, name);
|
|
2264
|
+
await fs.writeFile(dockerfilePath, dockerfileContent, 'utf8');
|
|
2265
|
+
dockerfileCreated = true;
|
|
2266
|
+
|
|
2267
|
+
console.error(`Created Dockerfile for ${framework} framework`);
|
|
2268
|
+
|
|
2269
|
+
// If React app, also create nginx.conf
|
|
2270
|
+
if (framework === 'react') {
|
|
2271
|
+
const nginxPath = path.join(local_path, 'nginx.conf');
|
|
2272
|
+
try {
|
|
2273
|
+
await fs.access(nginxPath);
|
|
2274
|
+
} catch {
|
|
2275
|
+
const nginxContent = generateNginxConf();
|
|
2276
|
+
await fs.writeFile(nginxPath, nginxContent, 'utf8');
|
|
2277
|
+
console.error('Created nginx.conf for React app');
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
console.error(`Failed to create Dockerfile: ${err.message}`);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2026
2285
|
response.webhook = {
|
|
2027
2286
|
secret: webhookSecret,
|
|
2028
2287
|
status: 'created',
|
|
@@ -2032,7 +2291,8 @@ async function initProject(args) {
|
|
|
2032
2291
|
response.deployment = {
|
|
2033
2292
|
status: 'initialized',
|
|
2034
2293
|
domain: `${name}.eu-central.mlgym.app`,
|
|
2035
|
-
message: 'Coolify deployment ready - push to main branch to trigger deployment'
|
|
2294
|
+
message: 'Coolify deployment ready - push to main branch to trigger deployment',
|
|
2295
|
+
dockerfile: dockerfileCreated ? 'Generated automatically based on framework detection' : 'Using existing Dockerfile'
|
|
2036
2296
|
};
|
|
2037
2297
|
}
|
|
2038
2298
|
|
|
@@ -2075,6 +2335,180 @@ function generateWebhookSecret() {
|
|
|
2075
2335
|
return secret;
|
|
2076
2336
|
}
|
|
2077
2337
|
|
|
2338
|
+
// Generate Dockerfile based on detected framework
|
|
2339
|
+
function generateDockerfile(framework, projectName) {
|
|
2340
|
+
const dockerfiles = {
|
|
2341
|
+
nextjs: `# Next.js Production Dockerfile
|
|
2342
|
+
FROM node:20-alpine AS builder
|
|
2343
|
+
WORKDIR /app
|
|
2344
|
+
COPY package*.json ./
|
|
2345
|
+
RUN npm ci
|
|
2346
|
+
COPY . .
|
|
2347
|
+
RUN npm run build
|
|
2348
|
+
|
|
2349
|
+
FROM node:20-alpine
|
|
2350
|
+
WORKDIR /app
|
|
2351
|
+
ENV NODE_ENV=production
|
|
2352
|
+
COPY --from=builder /app/public ./public
|
|
2353
|
+
COPY --from=builder /app/.next ./.next
|
|
2354
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
2355
|
+
COPY --from=builder /app/package.json ./package.json
|
|
2356
|
+
EXPOSE 3000
|
|
2357
|
+
ENV PORT=3000
|
|
2358
|
+
CMD ["npm", "start"]`,
|
|
2359
|
+
|
|
2360
|
+
react: `# React Production Dockerfile
|
|
2361
|
+
FROM node:20-alpine AS builder
|
|
2362
|
+
WORKDIR /app
|
|
2363
|
+
COPY package*.json ./
|
|
2364
|
+
RUN npm ci
|
|
2365
|
+
COPY . .
|
|
2366
|
+
RUN npm run build
|
|
2367
|
+
|
|
2368
|
+
FROM nginx:alpine
|
|
2369
|
+
COPY --from=builder /app/build /usr/share/nginx/html
|
|
2370
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
2371
|
+
EXPOSE 80
|
|
2372
|
+
CMD ["nginx", "-g", "daemon off;"]`,
|
|
2373
|
+
|
|
2374
|
+
express: `# Express.js Dockerfile
|
|
2375
|
+
FROM node:20-alpine
|
|
2376
|
+
WORKDIR /app
|
|
2377
|
+
COPY package*.json ./
|
|
2378
|
+
RUN npm ci
|
|
2379
|
+
COPY . .
|
|
2380
|
+
EXPOSE 3000
|
|
2381
|
+
ENV PORT=3000
|
|
2382
|
+
CMD ["node", "index.js"]`,
|
|
2383
|
+
|
|
2384
|
+
django: `# Django Dockerfile
|
|
2385
|
+
FROM python:3.11-slim
|
|
2386
|
+
WORKDIR /app
|
|
2387
|
+
ENV PYTHONUNBUFFERED=1
|
|
2388
|
+
COPY requirements.txt .
|
|
2389
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
2390
|
+
COPY . .
|
|
2391
|
+
RUN python manage.py collectstatic --noinput
|
|
2392
|
+
EXPOSE 8000
|
|
2393
|
+
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]`,
|
|
2394
|
+
|
|
2395
|
+
php: `# PHP Dockerfile
|
|
2396
|
+
FROM php:8.2-apache
|
|
2397
|
+
RUN docker-php-ext-install pdo pdo_mysql
|
|
2398
|
+
COPY . /var/www/html/
|
|
2399
|
+
RUN a2enmod rewrite
|
|
2400
|
+
EXPOSE 80`,
|
|
2401
|
+
|
|
2402
|
+
static: `# Static Site Dockerfile
|
|
2403
|
+
FROM nginx:alpine
|
|
2404
|
+
COPY . /usr/share/nginx/html
|
|
2405
|
+
EXPOSE 80
|
|
2406
|
+
CMD ["nginx", "-g", "daemon off;"]`,
|
|
2407
|
+
|
|
2408
|
+
go: `# Go Dockerfile
|
|
2409
|
+
FROM golang:1.21-alpine AS builder
|
|
2410
|
+
WORKDIR /app
|
|
2411
|
+
COPY go.* ./
|
|
2412
|
+
RUN go mod download
|
|
2413
|
+
COPY . .
|
|
2414
|
+
RUN go build -o main .
|
|
2415
|
+
|
|
2416
|
+
FROM alpine:latest
|
|
2417
|
+
RUN apk --no-cache add ca-certificates
|
|
2418
|
+
WORKDIR /root/
|
|
2419
|
+
COPY --from=builder /app/main .
|
|
2420
|
+
EXPOSE 8080
|
|
2421
|
+
CMD ["./main"]`,
|
|
2422
|
+
|
|
2423
|
+
rust: `# Rust Dockerfile
|
|
2424
|
+
FROM rust:1.75 AS builder
|
|
2425
|
+
WORKDIR /app
|
|
2426
|
+
COPY Cargo.* ./
|
|
2427
|
+
COPY src ./src
|
|
2428
|
+
RUN cargo build --release
|
|
2429
|
+
|
|
2430
|
+
FROM debian:bookworm-slim
|
|
2431
|
+
WORKDIR /app
|
|
2432
|
+
COPY --from=builder /app/target/release/${projectName} .
|
|
2433
|
+
EXPOSE 8080
|
|
2434
|
+
CMD ["./${projectName}"]`
|
|
2435
|
+
};
|
|
2436
|
+
|
|
2437
|
+
return dockerfiles[framework] || dockerfiles.static;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// Generate nginx.conf for React apps
|
|
2441
|
+
function generateNginxConf() {
|
|
2442
|
+
return `server {
|
|
2443
|
+
listen 80;
|
|
2444
|
+
location / {
|
|
2445
|
+
root /usr/share/nginx/html;
|
|
2446
|
+
index index.html index.htm;
|
|
2447
|
+
try_files $uri $uri/ /index.html;
|
|
2448
|
+
}
|
|
2449
|
+
}`;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// Detect project framework by analyzing files
|
|
2453
|
+
async function detectFramework(localPath) {
|
|
2454
|
+
try {
|
|
2455
|
+
// Check for package.json
|
|
2456
|
+
const packagePath = path.join(localPath, 'package.json');
|
|
2457
|
+
const packageData = await fs.readFile(packagePath, 'utf8');
|
|
2458
|
+
const packageJson = JSON.parse(packageData);
|
|
2459
|
+
|
|
2460
|
+
// Check dependencies
|
|
2461
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
2462
|
+
|
|
2463
|
+
if (deps.next) return 'nextjs';
|
|
2464
|
+
if (deps.react && !deps.next) return 'react';
|
|
2465
|
+
if (deps.express) return 'express';
|
|
2466
|
+
if (deps.vue) return 'vue';
|
|
2467
|
+
if (deps.angular) return 'angular';
|
|
2468
|
+
|
|
2469
|
+
// If has package.json but no recognized framework
|
|
2470
|
+
return 'node';
|
|
2471
|
+
} catch {
|
|
2472
|
+
// No package.json, check for other files
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// Check for Python
|
|
2476
|
+
try {
|
|
2477
|
+
await fs.access(path.join(localPath, 'requirements.txt'));
|
|
2478
|
+
const content = await fs.readFile(path.join(localPath, 'requirements.txt'), 'utf8');
|
|
2479
|
+
if (content.includes('django')) return 'django';
|
|
2480
|
+
if (content.includes('flask')) return 'flask';
|
|
2481
|
+
return 'python';
|
|
2482
|
+
} catch {}
|
|
2483
|
+
|
|
2484
|
+
// Check for Go
|
|
2485
|
+
try {
|
|
2486
|
+
await fs.access(path.join(localPath, 'go.mod'));
|
|
2487
|
+
return 'go';
|
|
2488
|
+
} catch {}
|
|
2489
|
+
|
|
2490
|
+
// Check for Rust
|
|
2491
|
+
try {
|
|
2492
|
+
await fs.access(path.join(localPath, 'Cargo.toml'));
|
|
2493
|
+
return 'rust';
|
|
2494
|
+
} catch {}
|
|
2495
|
+
|
|
2496
|
+
// Check for PHP
|
|
2497
|
+
try {
|
|
2498
|
+
await fs.access(path.join(localPath, 'composer.json'));
|
|
2499
|
+
return 'php';
|
|
2500
|
+
} catch {}
|
|
2501
|
+
|
|
2502
|
+
// Check for index.php
|
|
2503
|
+
try {
|
|
2504
|
+
await fs.access(path.join(localPath, 'index.php'));
|
|
2505
|
+
return 'php';
|
|
2506
|
+
} catch {}
|
|
2507
|
+
|
|
2508
|
+
// Default to static site
|
|
2509
|
+
return 'static';
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2078
2512
|
// Tool implementation: Recover User
|
|
2079
2513
|
async function recoverUser(args) {
|
|
2080
2514
|
const { email, pin, new_password } = args;
|
|
@@ -5893,14 +6327,33 @@ async function replicateProject(args) {
|
|
|
5893
6327
|
|
|
5894
6328
|
// Start the server
|
|
5895
6329
|
async function main() {
|
|
6330
|
+
// Check for updates BEFORE starting the server
|
|
6331
|
+
const updateCheck = await checkForUpdates();
|
|
6332
|
+
|
|
6333
|
+
if (updateCheck.updateAvailable) {
|
|
6334
|
+
// Block execution and show update required message
|
|
6335
|
+
console.error(`\n╔════════════════════════════════════════════════════════════════════╗`);
|
|
6336
|
+
console.error(`║ 🚫 UPDATE REQUIRED 🚫 ║`);
|
|
6337
|
+
console.error(`║ ║`);
|
|
6338
|
+
console.error(`║ The MLGym MCP Server is out of date and cannot run. ║`);
|
|
6339
|
+
console.error(`║ ║`);
|
|
6340
|
+
console.error(`║ Current version: ${CURRENT_VERSION.padEnd(52)} ║`);
|
|
6341
|
+
console.error(`║ Required version: ${updateCheck.latest.padEnd(51)} ║`);
|
|
6342
|
+
console.error(`║ ║`);
|
|
6343
|
+
console.error(`║ Please update before continuing: ║`);
|
|
6344
|
+
console.error(`║ ║`);
|
|
6345
|
+
console.error(`║ npm install -g ${PACKAGE_NAME}@latest ║`);
|
|
6346
|
+
console.error(`║ ║`);
|
|
6347
|
+
console.error(`║ Then restart your IDE or MCP client. ║`);
|
|
6348
|
+
console.error(`╚════════════════════════════════════════════════════════════════════════╝\n`);
|
|
6349
|
+
|
|
6350
|
+
// Exit with error code to prevent MCP from starting
|
|
6351
|
+
process.exit(1);
|
|
6352
|
+
}
|
|
6353
|
+
|
|
5896
6354
|
const transport = new StdioServerTransport();
|
|
5897
6355
|
await server.connect(transport);
|
|
5898
6356
|
console.error(`GitLab Backend MCP Server v${CURRENT_VERSION} started`);
|
|
5899
|
-
|
|
5900
|
-
// Check for updates in the background (don't block startup)
|
|
5901
|
-
setTimeout(async () => {
|
|
5902
|
-
await checkForUpdates();
|
|
5903
|
-
}, 2000); // Check after 2 seconds
|
|
5904
6357
|
}
|
|
5905
6358
|
|
|
5906
6359
|
main().catch((error) => {
|