mlgym-deploy 3.3.40 → 3.3.42
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 +336 -2
- package/package.json +5 -1
- package/tests/README.md +7 -2
- package/tests/deploy_dollie_test.sh +199 -0
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.42'; // Add deploy_dollie tool for Next.js dev containers with SSH
|
|
22
22
|
const PACKAGE_NAME = 'mlgym-deploy';
|
|
23
23
|
|
|
24
24
|
// Debug logging configuration - ENABLED BY DEFAULT
|
|
@@ -1949,6 +1949,42 @@ async function detectMissingNodeDependencies(projectPath) {
|
|
|
1949
1949
|
return null;
|
|
1950
1950
|
}
|
|
1951
1951
|
|
|
1952
|
+
/**
|
|
1953
|
+
* Detect invalid path alias dependencies in package.json
|
|
1954
|
+
* Path aliases like "@/components" should only exist in tsconfig.json, not as npm packages
|
|
1955
|
+
*/
|
|
1956
|
+
function detectInvalidPathAliasDependencies(packageJsonPath) {
|
|
1957
|
+
try {
|
|
1958
|
+
const packageJsonContent = fsSync.readFileSync(packageJsonPath, 'utf-8');
|
|
1959
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
1960
|
+
|
|
1961
|
+
const invalidDeps = [];
|
|
1962
|
+
const allDeps = {
|
|
1963
|
+
...packageJson.dependencies || {},
|
|
1964
|
+
...packageJson.devDependencies || {}
|
|
1965
|
+
};
|
|
1966
|
+
|
|
1967
|
+
// Check for path alias patterns like "@/something"
|
|
1968
|
+
for (const [depName, depVersion] of Object.entries(allDeps)) {
|
|
1969
|
+
if (depName.startsWith('@/')) {
|
|
1970
|
+
invalidDeps.push(depName);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
if (invalidDeps.length > 0) {
|
|
1975
|
+
return {
|
|
1976
|
+
invalid: true,
|
|
1977
|
+
packages: invalidDeps,
|
|
1978
|
+
autofix: 'remove_path_aliases'
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
log.warning(`MCP >>> Path alias validation failed: ${err.message}`);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
return null;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1952
1988
|
// Validate Dockerfile for Coolify compliance
|
|
1953
1989
|
function validateDockerfile(content) {
|
|
1954
1990
|
const lines = content.split('\n');
|
|
@@ -3299,6 +3335,51 @@ async function deployProject(args) {
|
|
|
3299
3335
|
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
3300
3336
|
log.success(`MCP >>> Added missing dependencies: ${missingDeps.packages.join(', ')}`);
|
|
3301
3337
|
}
|
|
3338
|
+
|
|
3339
|
+
// Step 4.6: Check for invalid path alias dependencies
|
|
3340
|
+
log.info('MCP >>> Checking for invalid path alias dependencies...');
|
|
3341
|
+
const invalidDeps = detectInvalidPathAliasDependencies(packageJsonPath);
|
|
3342
|
+
if (invalidDeps && invalidDeps.invalid && invalidDeps.packages.length > 0) {
|
|
3343
|
+
log.warning(`MCP >>> ⚠️ CRITICAL: Invalid path alias dependencies detected!`);
|
|
3344
|
+
log.warning(`MCP >>> The following entries should NOT be in package.json:`);
|
|
3345
|
+
invalidDeps.packages.forEach(pkg => log.warning(`MCP >>> - ${pkg}`));
|
|
3346
|
+
log.warning(`MCP >>> `);
|
|
3347
|
+
log.warning(`MCP >>> ❌ ERROR: npm cannot install packages with names like "@/components"`);
|
|
3348
|
+
log.warning(`MCP >>> These are TypeScript/JavaScript path aliases, not npm packages!`);
|
|
3349
|
+
log.warning(`MCP >>> `);
|
|
3350
|
+
log.warning(`MCP >>> ✅ HOW TO FIX:`);
|
|
3351
|
+
log.warning(`MCP >>> 1. Remove these entries from package.json dependencies/devDependencies`);
|
|
3352
|
+
log.warning(`MCP >>> 2. Keep them ONLY in tsconfig.json or jsconfig.json under "paths":`);
|
|
3353
|
+
log.warning(`MCP >>> {`);
|
|
3354
|
+
log.warning(`MCP >>> "compilerOptions": {`);
|
|
3355
|
+
log.warning(`MCP >>> "paths": { "@/*": ["./*"] }`);
|
|
3356
|
+
log.warning(`MCP >>> }`);
|
|
3357
|
+
log.warning(`MCP >>> }`);
|
|
3358
|
+
log.warning(`MCP >>> `);
|
|
3359
|
+
log.info('MCP >>> Auto-fixing: Removing invalid path alias dependencies from package.json');
|
|
3360
|
+
|
|
3361
|
+
const packageJsonContent = fsSync.readFileSync(packageJsonPath, 'utf-8');
|
|
3362
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
3363
|
+
|
|
3364
|
+
// Remove invalid dependencies
|
|
3365
|
+
let removedCount = 0;
|
|
3366
|
+
for (const pkg of invalidDeps.packages) {
|
|
3367
|
+
if (packageJson.dependencies && packageJson.dependencies[pkg]) {
|
|
3368
|
+
delete packageJson.dependencies[pkg];
|
|
3369
|
+
removedCount++;
|
|
3370
|
+
}
|
|
3371
|
+
if (packageJson.devDependencies && packageJson.devDependencies[pkg]) {
|
|
3372
|
+
delete packageJson.devDependencies[pkg];
|
|
3373
|
+
removedCount++;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// Create backup and save fixed version
|
|
3378
|
+
fsSync.writeFileSync(packageJsonPath + '.backup', packageJsonContent);
|
|
3379
|
+
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
3380
|
+
log.success(`MCP >>> ✅ Removed ${removedCount} invalid path alias dependencies`);
|
|
3381
|
+
log.info(`MCP >>> Backup saved to: package.json.backup`);
|
|
3382
|
+
}
|
|
3302
3383
|
}
|
|
3303
3384
|
|
|
3304
3385
|
// Step 5: Create GitLab project and deploy
|
|
@@ -3353,6 +3434,205 @@ async function deployProject(args) {
|
|
|
3353
3434
|
}
|
|
3354
3435
|
}
|
|
3355
3436
|
|
|
3437
|
+
// ============================================================================
|
|
3438
|
+
// DOLLIE DEPLOYMENT - Next.js Dev Containers with SSH
|
|
3439
|
+
// ============================================================================
|
|
3440
|
+
async function deployDollie(args) {
|
|
3441
|
+
const {
|
|
3442
|
+
project_name,
|
|
3443
|
+
project_description,
|
|
3444
|
+
git_repo_url,
|
|
3445
|
+
git_branch = '',
|
|
3446
|
+
git_token = '',
|
|
3447
|
+
app_port = 3000,
|
|
3448
|
+
email,
|
|
3449
|
+
password
|
|
3450
|
+
} = args;
|
|
3451
|
+
|
|
3452
|
+
log.info('MCP >>> Starting Dollie deployment workflow');
|
|
3453
|
+
log.debug('MCP >>> Arguments:', { project_name, git_repo_url, git_branch, app_port });
|
|
3454
|
+
|
|
3455
|
+
try {
|
|
3456
|
+
// Step 1: Authenticate
|
|
3457
|
+
log.info('MCP >>> STEP 1: Authenticating...');
|
|
3458
|
+
const auth = await loadAuth();
|
|
3459
|
+
|
|
3460
|
+
// If not authenticated and credentials provided, authenticate
|
|
3461
|
+
if (!auth.token && email && password) {
|
|
3462
|
+
const loginResult = await apiRequest('POST', '/api/v1/auth/login', {
|
|
3463
|
+
email,
|
|
3464
|
+
password
|
|
3465
|
+
}, false);
|
|
3466
|
+
|
|
3467
|
+
if (!loginResult.success || !loginResult.data.token) {
|
|
3468
|
+
return {
|
|
3469
|
+
content: [{
|
|
3470
|
+
type: 'text',
|
|
3471
|
+
text: JSON.stringify({
|
|
3472
|
+
status: 'error',
|
|
3473
|
+
message: 'Authentication failed',
|
|
3474
|
+
error: loginResult.error || 'Invalid credentials',
|
|
3475
|
+
next_steps: [
|
|
3476
|
+
'Provide valid email and password',
|
|
3477
|
+
'Or ensure you are already logged in'
|
|
3478
|
+
]
|
|
3479
|
+
}, null, 2)
|
|
3480
|
+
}]
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
await saveAuth(email, loginResult.data.token);
|
|
3485
|
+
log.success('MCP >>> STEP 1: Authentication successful');
|
|
3486
|
+
} else if (!auth.token) {
|
|
3487
|
+
return {
|
|
3488
|
+
content: [{
|
|
3489
|
+
type: 'text',
|
|
3490
|
+
text: JSON.stringify({
|
|
3491
|
+
status: 'error',
|
|
3492
|
+
message: 'Authentication required',
|
|
3493
|
+
error: 'No authentication token found and no credentials provided',
|
|
3494
|
+
next_steps: [
|
|
3495
|
+
'Provide email and password in the request',
|
|
3496
|
+
'Or use mlgym_auth_login first'
|
|
3497
|
+
]
|
|
3498
|
+
}, null, 2)
|
|
3499
|
+
}]
|
|
3500
|
+
};
|
|
3501
|
+
} else {
|
|
3502
|
+
log.success('MCP >>> STEP 1: Using existing authentication');
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
// Step 2: Get current user info
|
|
3506
|
+
log.info('MCP >>> STEP 2: Getting user information...');
|
|
3507
|
+
const userResult = await apiRequest('GET', '/api/v1/user', null, true);
|
|
3508
|
+
|
|
3509
|
+
if (!userResult.success) {
|
|
3510
|
+
return {
|
|
3511
|
+
content: [{
|
|
3512
|
+
type: 'text',
|
|
3513
|
+
text: JSON.stringify({
|
|
3514
|
+
status: 'error',
|
|
3515
|
+
message: 'Failed to get user information',
|
|
3516
|
+
error: userResult.error
|
|
3517
|
+
}, null, 2)
|
|
3518
|
+
}]
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
log.success('MCP >>> STEP 2: User information retrieved');
|
|
3523
|
+
|
|
3524
|
+
// Step 3: Generate SSH password
|
|
3525
|
+
log.info('MCP >>> STEP 3: Generating SSH password...');
|
|
3526
|
+
const sshPassword = generateRandomPassword();
|
|
3527
|
+
log.success('MCP >>> STEP 3: SSH password generated');
|
|
3528
|
+
|
|
3529
|
+
// Step 4: Prepare environment variables
|
|
3530
|
+
log.info('MCP >>> STEP 4: Preparing environment variables...');
|
|
3531
|
+
const environmentVariables = {
|
|
3532
|
+
GIT_REPO_URL: git_repo_url,
|
|
3533
|
+
GIT_BRANCH: git_branch,
|
|
3534
|
+
GIT_TOKEN: git_token,
|
|
3535
|
+
SSH_PASSWORD: sshPassword,
|
|
3536
|
+
S5_USERNAME: 'developer',
|
|
3537
|
+
APP_PORT: app_port.toString(),
|
|
3538
|
+
SSH_PORT: '22'
|
|
3539
|
+
};
|
|
3540
|
+
log.success('MCP >>> STEP 4: Environment variables prepared');
|
|
3541
|
+
log.debug('MCP >>> Environment variables:', Object.keys(environmentVariables));
|
|
3542
|
+
|
|
3543
|
+
// Step 5: Create Coolify resource with docker-image
|
|
3544
|
+
log.info('MCP >>> STEP 5: Creating Coolify deployment...');
|
|
3545
|
+
const createResult = await apiRequest('POST', '/api/v1/projects', {
|
|
3546
|
+
name: project_name,
|
|
3547
|
+
description: project_description,
|
|
3548
|
+
repository: '', // Not needed for docker-image deployments
|
|
3549
|
+
branch: '',
|
|
3550
|
+
build_pack: 'docker-image',
|
|
3551
|
+
docker_image: 'code.stratus5.com:5050/dollie/dollie/nextjs-dev:latest',
|
|
3552
|
+
environment_variables: environmentVariables,
|
|
3553
|
+
ports_exposes: app_port.toString()
|
|
3554
|
+
}, true);
|
|
3555
|
+
|
|
3556
|
+
if (!createResult.success) {
|
|
3557
|
+
return {
|
|
3558
|
+
content: [{
|
|
3559
|
+
type: 'text',
|
|
3560
|
+
text: JSON.stringify({
|
|
3561
|
+
status: 'error',
|
|
3562
|
+
message: 'Failed to create Dollie deployment',
|
|
3563
|
+
error: createResult.error,
|
|
3564
|
+
step: 'create_coolify_resource'
|
|
3565
|
+
}, null, 2)
|
|
3566
|
+
}]
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
log.success('MCP >>> STEP 5: Coolify deployment created');
|
|
3571
|
+
|
|
3572
|
+
// Step 6: Extract deployment details
|
|
3573
|
+
const deployment = createResult.data;
|
|
3574
|
+
const appUuid = deployment.coolify_uuid;
|
|
3575
|
+
const appUrl = deployment.url;
|
|
3576
|
+
|
|
3577
|
+
// Parse URL to extract hostname
|
|
3578
|
+
const urlObj = new URL(appUrl);
|
|
3579
|
+
const hostname = urlObj.hostname;
|
|
3580
|
+
|
|
3581
|
+
// Determine SSH port based on node (typically 2201 for all nodes)
|
|
3582
|
+
const sshPort = 2201;
|
|
3583
|
+
|
|
3584
|
+
log.success('MCP >>> Dollie deployment completed successfully');
|
|
3585
|
+
|
|
3586
|
+
return {
|
|
3587
|
+
content: [{
|
|
3588
|
+
type: 'text',
|
|
3589
|
+
text: JSON.stringify({
|
|
3590
|
+
success: true,
|
|
3591
|
+
deployment: {
|
|
3592
|
+
url: appUrl,
|
|
3593
|
+
ssh: {
|
|
3594
|
+
host: hostname,
|
|
3595
|
+
port: sshPort,
|
|
3596
|
+
username: 'developer',
|
|
3597
|
+
password: sshPassword
|
|
3598
|
+
},
|
|
3599
|
+
status: 'deploying',
|
|
3600
|
+
coolify_uuid: appUuid,
|
|
3601
|
+
project_name: project_name,
|
|
3602
|
+
git_repo: git_repo_url,
|
|
3603
|
+
git_branch: git_branch || 'default',
|
|
3604
|
+
app_port: app_port
|
|
3605
|
+
},
|
|
3606
|
+
next_steps: [
|
|
3607
|
+
`Container is deploying at ${appUrl}`,
|
|
3608
|
+
`SSH access: ssh developer@${hostname} -p ${sshPort}`,
|
|
3609
|
+
`Password: ${sshPassword}`,
|
|
3610
|
+
'The container will:',
|
|
3611
|
+
' 1. Clone your git repository',
|
|
3612
|
+
' 2. Install dependencies (npm/yarn/pnpm)',
|
|
3613
|
+
' 3. Start Next.js dev server',
|
|
3614
|
+
' 4. Enable SSH access',
|
|
3615
|
+
'Wait 2-3 minutes for deployment to complete'
|
|
3616
|
+
]
|
|
3617
|
+
}, null, 2)
|
|
3618
|
+
}]
|
|
3619
|
+
};
|
|
3620
|
+
|
|
3621
|
+
} catch (error) {
|
|
3622
|
+
log.error('MCP >>> Dollie deployment failed:', error.message);
|
|
3623
|
+
return {
|
|
3624
|
+
content: [{
|
|
3625
|
+
type: 'text',
|
|
3626
|
+
text: JSON.stringify({
|
|
3627
|
+
status: 'error',
|
|
3628
|
+
message: 'Dollie deployment failed',
|
|
3629
|
+
error: error.message
|
|
3630
|
+
}, null, 2)
|
|
3631
|
+
}]
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3356
3636
|
// ============================================================================
|
|
3357
3637
|
// SIMPLIFIED STATUS CHECK
|
|
3358
3638
|
// ============================================================================
|
|
@@ -4686,6 +4966,55 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4686
4966
|
}
|
|
4687
4967
|
}
|
|
4688
4968
|
},
|
|
4969
|
+
{
|
|
4970
|
+
name: 'deploy_dollie',
|
|
4971
|
+
description: 'Deploy a Next.js development container with SSH access for AI-assisted development. Uses the Dollie base image which clones your git repository and runs the dev server. Supports any public git repo or private repos with token authentication.',
|
|
4972
|
+
inputSchema: {
|
|
4973
|
+
type: 'object',
|
|
4974
|
+
properties: {
|
|
4975
|
+
project_name: {
|
|
4976
|
+
type: 'string',
|
|
4977
|
+
description: 'Project name (lowercase alphanumeric with hyphens, e.g., "my-nextjs-app")',
|
|
4978
|
+
pattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$',
|
|
4979
|
+
minLength: 3
|
|
4980
|
+
},
|
|
4981
|
+
project_description: {
|
|
4982
|
+
type: 'string',
|
|
4983
|
+
description: 'Brief project description (min 10 characters)',
|
|
4984
|
+
minLength: 10
|
|
4985
|
+
},
|
|
4986
|
+
git_repo_url: {
|
|
4987
|
+
type: 'string',
|
|
4988
|
+
description: 'Git repository URL (https://github.com/user/repo.git)',
|
|
4989
|
+
pattern: '^https://.*\\.git$'
|
|
4990
|
+
},
|
|
4991
|
+
git_branch: {
|
|
4992
|
+
type: 'string',
|
|
4993
|
+
description: 'Branch to clone (optional, defaults to repo\'s default branch)'
|
|
4994
|
+
},
|
|
4995
|
+
git_token: {
|
|
4996
|
+
type: 'string',
|
|
4997
|
+
description: 'OAuth token for private repos (optional)'
|
|
4998
|
+
},
|
|
4999
|
+
app_port: {
|
|
5000
|
+
type: 'number',
|
|
5001
|
+
description: 'Next.js port inside container (optional, default: 3000)',
|
|
5002
|
+
default: 3000
|
|
5003
|
+
},
|
|
5004
|
+
email: {
|
|
5005
|
+
type: 'string',
|
|
5006
|
+
description: 'Email for authentication (optional if already authenticated)',
|
|
5007
|
+
pattern: '^[^@]+@[^@]+\\.[^@]+$'
|
|
5008
|
+
},
|
|
5009
|
+
password: {
|
|
5010
|
+
type: 'string',
|
|
5011
|
+
description: 'Password (optional if already authenticated, min 8 characters)',
|
|
5012
|
+
minLength: 8
|
|
5013
|
+
}
|
|
5014
|
+
},
|
|
5015
|
+
required: ['project_name', 'project_description', 'git_repo_url']
|
|
5016
|
+
}
|
|
5017
|
+
},
|
|
4689
5018
|
{
|
|
4690
5019
|
name: 'mlgym_help',
|
|
4691
5020
|
description: 'Display all available MLGym tools with descriptions and usage examples. Use this to discover what operations you can perform.',
|
|
@@ -4821,13 +5150,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4821
5150
|
result = await rollback(args);
|
|
4822
5151
|
break;
|
|
4823
5152
|
|
|
5153
|
+
case 'deploy_dollie':
|
|
5154
|
+
log.info(`Deploying Dollie development container...`);
|
|
5155
|
+
result = await deployDollie(args);
|
|
5156
|
+
break;
|
|
5157
|
+
|
|
4824
5158
|
case 'mlgym_help':
|
|
4825
5159
|
log.info(`Showing help...`);
|
|
4826
5160
|
result = await showHelp(args);
|
|
4827
5161
|
break;
|
|
4828
5162
|
|
|
4829
5163
|
default:
|
|
4830
|
-
throw new Error(`Unknown tool: ${name}. Available tools: mlgym_deploy, mlgym_status, mlgym_deploy_logs, mlgym_user_create, mlgym_auth_login, mlgym_set_env_vars, mlgym_set_health_check, mlgym_set_domain, mlgym_set_deployment_commands, mlgym_deploy_manual, mlgym_set_options, mlgym_rollback`);
|
|
5164
|
+
throw new Error(`Unknown tool: ${name}. Available tools: mlgym_deploy, mlgym_status, mlgym_deploy_logs, deploy_dollie, mlgym_user_create, mlgym_auth_login, mlgym_set_env_vars, mlgym_set_health_check, mlgym_set_domain, mlgym_set_deployment_commands, mlgym_deploy_manual, mlgym_set_options, mlgym_rollback`);
|
|
4831
5165
|
}
|
|
4832
5166
|
|
|
4833
5167
|
const duration = Date.now() - startTime;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mlgym-deploy",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.42",
|
|
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",
|
|
@@ -80,6 +80,10 @@
|
|
|
80
80
|
"name": "mlgym_rollback",
|
|
81
81
|
"description": "Rollback to previous deployment"
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
"name": "deploy_dollie",
|
|
85
|
+
"description": "Deploy Next.js development container with SSH access using pre-built Dollie image"
|
|
86
|
+
},
|
|
83
87
|
{
|
|
84
88
|
"name": "mlgym_help",
|
|
85
89
|
"description": "Display all available tools with descriptions and usage examples"
|
package/tests/README.md
CHANGED
|
@@ -13,7 +13,8 @@ tests/
|
|
|
13
13
|
├── mlgym_auth_logout_test.sh # ✅ 8 tests (auth_logout)
|
|
14
14
|
├── mlgym_project_init_test.sh # ✅ 6 tests (project_init)
|
|
15
15
|
├── mlgym_projects_list_test.sh # ✅ 6 tests (projects_list)
|
|
16
|
-
|
|
16
|
+
├── mlgym_projects_get_test.sh # ✅ 7 tests (projects_get)
|
|
17
|
+
└── deploy_dollie_test.sh # ✅ 20 tests (deploy_dollie)
|
|
17
18
|
```
|
|
18
19
|
|
|
19
20
|
## Running Tests
|
|
@@ -32,7 +33,7 @@ cd mcp-server
|
|
|
32
33
|
|
|
33
34
|
## Test Coverage
|
|
34
35
|
|
|
35
|
-
### Completed (
|
|
36
|
+
### Completed (7 functions, 82 total tests)
|
|
36
37
|
- ✅ **mlgym_auth_login** (15 tests)
|
|
37
38
|
- Valid/invalid credentials, SQL injection, XSS, buffer overflow, token storage
|
|
38
39
|
- ✅ **mlgym_user_create** (20 tests)
|
|
@@ -45,6 +46,10 @@ cd mcp-server
|
|
|
45
46
|
- List projects, user isolation, response format
|
|
46
47
|
- ✅ **mlgym_projects_get** (7 tests)
|
|
47
48
|
- Get project details, validation, non-existent projects
|
|
49
|
+
- ✅ **deploy_dollie** (20 tests)
|
|
50
|
+
- Authentication, deployment workflow, public repos, custom ports/branches
|
|
51
|
+
- Required fields, validation, security (SQL injection, XSS, buffer overflow)
|
|
52
|
+
- Response format, SSH password strength
|
|
48
53
|
|
|
49
54
|
### Remaining (37 functions)
|
|
50
55
|
See TODO.md for priority order and testing plan.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Comprehensive test suite for deploy_dollie
|
|
3
|
+
# Tests functional requirements, edge cases, and security
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
PASSED=0
|
|
8
|
+
FAILED=0
|
|
9
|
+
|
|
10
|
+
echo "=== deploy_dollie Comprehensive Test Suite ==="
|
|
11
|
+
echo ""
|
|
12
|
+
|
|
13
|
+
# Helper to run test
|
|
14
|
+
run_test() {
|
|
15
|
+
local test_name="$1"
|
|
16
|
+
local project_name="$2"
|
|
17
|
+
local project_description="$3"
|
|
18
|
+
local git_repo_url="$4"
|
|
19
|
+
local git_branch="$5"
|
|
20
|
+
local app_port="$6"
|
|
21
|
+
local expected_result="$7" # "success" or "error"
|
|
22
|
+
|
|
23
|
+
# Build JSON request
|
|
24
|
+
local branch_field=""
|
|
25
|
+
if [ -n "$git_branch" ]; then
|
|
26
|
+
branch_field=",\"git_branch\":\"$git_branch\""
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
local port_field=""
|
|
30
|
+
if [ -n "$app_port" ]; then
|
|
31
|
+
port_field=",\"app_port\":$app_port"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
local req="{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"deploy_dollie\",\"arguments\":{\"project_name\":\"$project_name\",\"project_description\":\"$project_description\",\"git_repo_url\":\"$git_repo_url\"$branch_field$port_field}},\"id\":99}"
|
|
35
|
+
local resp=$(echo "$req" | node index.js 2>/dev/null | tail -1)
|
|
36
|
+
|
|
37
|
+
# Check for deployment details (success) or error message
|
|
38
|
+
local content=$(echo "$resp" | jq -r '.result.content[0].text' 2>/dev/null)
|
|
39
|
+
local success=$(echo "$content" | jq -r '.success' 2>/dev/null)
|
|
40
|
+
local error_msg=$(echo "$content" | jq -r '.message' 2>/dev/null)
|
|
41
|
+
|
|
42
|
+
if [ "$expected_result" = "success" ]; then
|
|
43
|
+
if [ "$success" = "true" ]; then
|
|
44
|
+
local ssh_host=$(echo "$content" | jq -r '.deployment.ssh.host' 2>/dev/null)
|
|
45
|
+
local ssh_password=$(echo "$content" | jq -r '.deployment.ssh.password' 2>/dev/null)
|
|
46
|
+
echo "✅ $test_name (host: $ssh_host, password: ${ssh_password:0:8}...)"
|
|
47
|
+
PASSED=$((PASSED + 1))
|
|
48
|
+
else
|
|
49
|
+
echo "❌ $test_name (expected deployment success, got: $error_msg)"
|
|
50
|
+
FAILED=$((FAILED + 1))
|
|
51
|
+
fi
|
|
52
|
+
else
|
|
53
|
+
# Expected error
|
|
54
|
+
if echo "$content" | grep -qE "error|missing|required|invalid|must|Authentication"; then
|
|
55
|
+
echo "✅ $test_name"
|
|
56
|
+
PASSED=$((PASSED + 1))
|
|
57
|
+
else
|
|
58
|
+
echo "❌ $test_name (expected error, got success or no error)"
|
|
59
|
+
FAILED=$((FAILED + 1))
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Test 1: Authentication check - should fail without auth
|
|
65
|
+
echo "Test 1: Deployment without authentication"
|
|
66
|
+
TEST_PROJECT="dollie-test-$(date +%s)"
|
|
67
|
+
run_test "Deployment without auth" "$TEST_PROJECT" "Test deployment without authentication" "https://github.com/vercel/next.js.git" "canary" "3000" "error"
|
|
68
|
+
|
|
69
|
+
# Test 2: Authenticate first
|
|
70
|
+
echo "Test 2: Authenticate with test account"
|
|
71
|
+
LOGIN_REQ='{"jsonrpc":"2.0","method":"tools/call","params":{"name":"mlgym_auth_login","arguments":{"email":"test-dollie@ezb.net","password":"TestPassword123!"}},"id":100}'
|
|
72
|
+
LOGIN_RESP=$(echo "$LOGIN_REQ" | node index.js 2>/dev/null | tail -1)
|
|
73
|
+
LOGIN_STATUS=$(echo "$LOGIN_RESP" | jq -r '.result.content[0].text' | jq -r '.status' 2>/dev/null)
|
|
74
|
+
|
|
75
|
+
if [ "$LOGIN_STATUS" = "authenticated" ]; then
|
|
76
|
+
echo "✅ Test 2: Authentication successful"
|
|
77
|
+
PASSED=$((PASSED + 1))
|
|
78
|
+
else
|
|
79
|
+
# Try to create the user first
|
|
80
|
+
echo "Creating test user..."
|
|
81
|
+
CREATE_REQ='{"jsonrpc":"2.0","method":"tools/call","params":{"name":"mlgym_user_create","arguments":{"email":"test-dollie@ezb.net","name":"Dollie Test User","password":"TestPassword123!","accept_terms":true}},"id":101}'
|
|
82
|
+
CREATE_RESP=$(echo "$CREATE_REQ" | node index.js 2>/dev/null | tail -1)
|
|
83
|
+
|
|
84
|
+
# Try login again
|
|
85
|
+
LOGIN_RESP=$(echo "$LOGIN_REQ" | node index.js 2>/dev/null | tail -1)
|
|
86
|
+
LOGIN_STATUS=$(echo "$LOGIN_RESP" | jq -r '.result.content[0].text' | jq -r '.status' 2>/dev/null)
|
|
87
|
+
|
|
88
|
+
if [ "$LOGIN_STATUS" = "authenticated" ]; then
|
|
89
|
+
echo "✅ Test 2: Authentication successful (after user creation)"
|
|
90
|
+
PASSED=$((PASSED + 1))
|
|
91
|
+
else
|
|
92
|
+
echo "⚠️ Test 2: Could not authenticate - skipping authenticated tests"
|
|
93
|
+
FAILED=$((FAILED + 1))
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Test 3: Valid deployment with public repo
|
|
98
|
+
echo "Test 3: Valid deployment with public GitHub repo"
|
|
99
|
+
TEST_PROJECT="dollie-valid-$(date +%s)"
|
|
100
|
+
run_test "Valid public repo deployment" "$TEST_PROJECT" "Test Next.js deployment from public repo" "https://github.com/vercel/next-learn.git" "main" "3000" "success"
|
|
101
|
+
|
|
102
|
+
# Test 4: Missing project_name
|
|
103
|
+
run_test "Missing project_name" "" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
|
|
104
|
+
|
|
105
|
+
# Test 5: Missing project_description
|
|
106
|
+
run_test "Missing description" "test-project" "" "https://github.com/vercel/next.js.git" "" "" "error"
|
|
107
|
+
|
|
108
|
+
# Test 6: Missing git_repo_url
|
|
109
|
+
run_test "Missing git repo URL" "test-project" "Test deployment" "" "" "" "error"
|
|
110
|
+
|
|
111
|
+
# Test 7: Invalid project_name format (uppercase)
|
|
112
|
+
run_test "Invalid project name (uppercase)" "INVALID-NAME" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
|
|
113
|
+
|
|
114
|
+
# Test 8: Invalid project_name format (spaces)
|
|
115
|
+
run_test "Invalid project name (spaces)" "invalid name" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
|
|
116
|
+
|
|
117
|
+
# Test 9: Short project_name (< 3 chars)
|
|
118
|
+
run_test "Short project name" "ab" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
|
|
119
|
+
|
|
120
|
+
# Test 10: Short project_description (< 10 chars)
|
|
121
|
+
run_test "Short description" "test-project" "Short" "https://github.com/vercel/next.js.git" "" "" "error"
|
|
122
|
+
|
|
123
|
+
# Test 11: Invalid git URL format (not https)
|
|
124
|
+
run_test "Invalid git URL (not https)" "test-project" "Test deployment" "git@github.com:vercel/next.js.git" "" "" "error"
|
|
125
|
+
|
|
126
|
+
# Test 12: Invalid git URL format (no .git)
|
|
127
|
+
run_test "Invalid git URL (no .git)" "test-project" "Test deployment" "https://github.com/vercel/next.js" "" "" "error"
|
|
128
|
+
|
|
129
|
+
# Test 13: Custom app_port
|
|
130
|
+
echo "Test 13: Custom app_port (8080)"
|
|
131
|
+
TEST_PROJECT="dollie-port-$(date +%s)"
|
|
132
|
+
run_test "Custom app port 8080" "$TEST_PROJECT" "Test deployment with custom port" "https://github.com/vercel/next-learn.git" "main" "8080" "success"
|
|
133
|
+
|
|
134
|
+
# Test 14: Custom git_branch
|
|
135
|
+
echo "Test 14: Custom git_branch"
|
|
136
|
+
TEST_PROJECT="dollie-branch-$(date +%s)"
|
|
137
|
+
run_test "Custom git branch" "$TEST_PROJECT" "Test deployment with custom branch" "https://github.com/vercel/next-learn.git" "canary" "3000" "success"
|
|
138
|
+
|
|
139
|
+
# Test 15: SQL injection in project_name
|
|
140
|
+
run_test "SQL injection (project_name)" "'; DROP TABLE applications;--" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
|
|
141
|
+
|
|
142
|
+
# Test 16: SQL injection in git_repo_url
|
|
143
|
+
run_test "SQL injection (git_repo_url)" "test-project" "Test deployment" "https://github.com/vercel/next.js.git'; DROP TABLE--" "" "" "error"
|
|
144
|
+
|
|
145
|
+
# Test 17: XSS attempt in project_description
|
|
146
|
+
run_test "XSS attempt (description)" "test-project" "<script>alert('XSS')</script>" "https://github.com/vercel/next.js.git" "" "" "success"
|
|
147
|
+
|
|
148
|
+
# Test 18: Buffer overflow in project_name
|
|
149
|
+
echo "Test 18: Buffer overflow (very long project_name)"
|
|
150
|
+
LONG_NAME=$(python3 -c "print('a' * 1000)")
|
|
151
|
+
run_test "Buffer overflow (project_name)" "$LONG_NAME" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
|
|
152
|
+
|
|
153
|
+
# Test 19: Response format validation
|
|
154
|
+
echo "Test 19: Response format validation"
|
|
155
|
+
TEST_PROJECT="dollie-format-$(date +%s)"
|
|
156
|
+
REQ="{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"deploy_dollie\",\"arguments\":{\"project_name\":\"$TEST_PROJECT\",\"project_description\":\"Test deployment for response format validation\",\"git_repo_url\":\"https://github.com/vercel/next-learn.git\",\"git_branch\":\"main\"}},\"id\":102}"
|
|
157
|
+
RESP=$(echo "$REQ" | node index.js 2>/dev/null | tail -1)
|
|
158
|
+
CONTENT=$(echo "$RESP" | jq -r '.result.content[0].text')
|
|
159
|
+
|
|
160
|
+
HAS_SUCCESS=$(echo "$CONTENT" | jq -e '.success' > /dev/null 2>&1 && echo "yes" || echo "no")
|
|
161
|
+
HAS_URL=$(echo "$CONTENT" | jq -e '.deployment.url' > /dev/null 2>&1 && echo "yes" || echo "no")
|
|
162
|
+
HAS_SSH=$(echo "$CONTENT" | jq -e '.deployment.ssh' > /dev/null 2>&1 && echo "yes" || echo "no")
|
|
163
|
+
HAS_SSH_HOST=$(echo "$CONTENT" | jq -e '.deployment.ssh.host' > /dev/null 2>&1 && echo "yes" || echo "no")
|
|
164
|
+
HAS_SSH_PORT=$(echo "$CONTENT" | jq -e '.deployment.ssh.port' > /dev/null 2>&1 && echo "yes" || echo "no")
|
|
165
|
+
HAS_SSH_USERNAME=$(echo "$CONTENT" | jq -e '.deployment.ssh.username' > /dev/null 2>&1 && echo "yes" || echo "no")
|
|
166
|
+
HAS_SSH_PASSWORD=$(echo "$CONTENT" | jq -e '.deployment.ssh.password' > /dev/null 2>&1 && echo "yes" || echo "no")
|
|
167
|
+
|
|
168
|
+
if [ "$HAS_SUCCESS" = "yes" ] && [ "$HAS_URL" = "yes" ] && [ "$HAS_SSH" = "yes" ] && [ "$HAS_SSH_HOST" = "yes" ] && [ "$HAS_SSH_PORT" = "yes" ] && [ "$HAS_SSH_USERNAME" = "yes" ] && [ "$HAS_SSH_PASSWORD" = "yes" ]; then
|
|
169
|
+
echo "✅ Test 19: Response format valid"
|
|
170
|
+
PASSED=$((PASSED + 1))
|
|
171
|
+
else
|
|
172
|
+
echo "❌ Test 19: Response format invalid (success:$HAS_SUCCESS, url:$HAS_URL, ssh:$HAS_SSH, host:$HAS_SSH_HOST, port:$HAS_SSH_PORT, user:$HAS_SSH_USERNAME, pass:$HAS_SSH_PASSWORD)"
|
|
173
|
+
FAILED=$((FAILED + 1))
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Test 20: SSH password strength validation
|
|
177
|
+
echo "Test 20: SSH password strength validation"
|
|
178
|
+
SSH_PASSWORD=$(echo "$CONTENT" | jq -r '.deployment.ssh.password')
|
|
179
|
+
PASSWORD_LENGTH=${#SSH_PASSWORD}
|
|
180
|
+
if [ $PASSWORD_LENGTH -ge 16 ]; then
|
|
181
|
+
echo "✅ Test 20: SSH password is strong (length: $PASSWORD_LENGTH)"
|
|
182
|
+
PASSED=$((PASSED + 1))
|
|
183
|
+
else
|
|
184
|
+
echo "❌ Test 20: SSH password is weak (length: $PASSWORD_LENGTH, expected >= 16)"
|
|
185
|
+
FAILED=$((FAILED + 1))
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
echo ""
|
|
189
|
+
echo "==============================================="
|
|
190
|
+
echo "RESULTS: $PASSED passed, $FAILED failed (Total: $((PASSED + FAILED)))"
|
|
191
|
+
echo "==============================================="
|
|
192
|
+
|
|
193
|
+
if [ $FAILED -eq 0 ]; then
|
|
194
|
+
echo "✅ ALL TESTS PASSED"
|
|
195
|
+
exit 0
|
|
196
|
+
else
|
|
197
|
+
echo "⚠️ SOME TESTS FAILED"
|
|
198
|
+
exit 1
|
|
199
|
+
fi
|