/**
* DevOps Helper MCP Tools
* Exported for testing
*/
import { exec } from "child_process";
import { promisify } from "util";
import { access } from "fs/promises";
import { constants } from "fs";
const execAsync = promisify(exec);
// Helper to run commands safely - can be mocked in tests
export async function runCommand(cmd, options = {}) {
try {
const { stdout, stderr } = await execAsync(cmd, { timeout: 30000, ...options });
return { success: true, stdout: stdout.trim(), stderr: stderr.trim() };
} catch (error) {
return { success: false, error: error.message, stdout: error.stdout?.trim(), stderr: error.stderr?.trim() };
}
}
// Allow injecting a mock runCommand for testing
let commandRunner = runCommand;
export function setCommandRunner(runner) {
commandRunner = runner;
}
export function getCommandRunner() {
return commandRunner;
}
// ============================================
// GIT TOOLS
// ============================================
export async function gitStatusExplained() {
const result = await commandRunner("git status --porcelain -b");
if (!result.success) {
return { content: [{ type: "text", text: `Error: ${result.error}\n\nAre you in a git repository? Try: git init` }] };
}
const lines = result.stdout.split("\n").filter(Boolean);
if (lines.length === 0) {
return { content: [{ type: "text", text: "Not a git repository or empty output" }] };
}
const branchLine = lines[0];
const fileLines = lines.slice(1);
const meanings = {
"??": "Untracked - New file, not yet tracked by git. Use 'git add <file>' to track it.",
" M": "Modified - Changed but not staged. Use 'git add <file>' to stage it.",
"M ": "Staged - Ready to be committed. Use 'git commit -m \"message\"' to commit.",
"MM": "Modified & Staged - File was staged, then modified again.",
"A ": "Added - New file, staged and ready to commit.",
"D ": "Deleted - File was deleted. Stage with 'git add <file>'.",
" D": "Deleted (unstaged) - File deleted but not staged.",
"R ": "Renamed - File was renamed.",
"C ": "Copied - File was copied.",
"UU": "Conflict - Merge conflict! Edit the file to resolve.",
};
let output = `Branch: ${branchLine.replace("## ", "")}\n\n`;
if (fileLines.length === 0) {
output += "Working tree clean - no changes to commit.";
} else {
output += "Changes:\n";
for (const line of fileLines) {
const status = line.substring(0, 2);
const file = line.substring(3);
const meaning = meanings[status] || `Unknown status: ${status}`;
output += ` ${file}\n Status: ${meaning}\n\n`;
}
}
return { content: [{ type: "text", text: output }] };
}
export async function gitBranchExplained() {
const result = await commandRunner("git branch -a");
if (!result.success) {
return { content: [{ type: "text", text: `Error: ${result.error}` }] };
}
let output = `BRANCHES:\n${result.stdout}\n\n`;
output += `BRANCH WORKFLOW:\n`;
output += `1. main/master - Production code. Never commit directly here.\n`;
output += `2. develop - Integration branch (if used). Features merge here first.\n`;
output += `3. feature/* - Your working branches. Create with:\n`;
output += ` git checkout -b feature/my-feature-name\n\n`;
output += `TO CREATE A NEW BRANCH:\n`;
output += ` git checkout -b feature/your-feature-name\n\n`;
output += `TO SWITCH BRANCHES:\n`;
output += ` git checkout branch-name\n`;
return { content: [{ type: "text", text: output }] };
}
export async function gitCommitGuided({ message, stage_all }) {
if (!message || message.length < 10) {
return { content: [{ type: "text", text: "Commit message should be at least 10 characters.\n\nGood commit message format:\n- feat: add user login feature\n- fix: resolve null pointer in checkout\n- docs: update README setup instructions\n- refactor: simplify payment processing" }] };
}
let output = "";
if (stage_all) {
const stageResult = await commandRunner("git add -A");
if (!stageResult.success) {
return { content: [{ type: "text", text: `Failed to stage: ${stageResult.error}` }] };
}
output += "Staged all changes.\n";
}
const statusResult = await commandRunner("git diff --cached --name-only");
if (!statusResult.stdout) {
return { content: [{ type: "text", text: "Nothing staged to commit.\n\nFirst stage your changes:\n git add <file> - stage specific file\n git add -A - stage all changes" }] };
}
output += `Files to be committed:\n${statusResult.stdout}\n\n`;
const commitResult = await commandRunner(`git commit -m "${message.replace(/"/g, '\\"')}"`);
if (!commitResult.success) {
return { content: [{ type: "text", text: `Commit failed: ${commitResult.error}\n${commitResult.stderr}` }] };
}
output += `Committed successfully!\n\n`;
output += `Next steps:\n`;
output += `1. Push to remote: git push origin <branch-name>\n`;
output += `2. Create a PR: gh pr create\n`;
return { content: [{ type: "text", text: output }] };
}
// ============================================
// DOCKER TOOLS
// ============================================
export async function dockerCheckSetup() {
const checks = [];
const versionResult = await commandRunner("docker --version");
if (versionResult.success) {
checks.push(`[OK] Docker installed: ${versionResult.stdout}`);
} else {
checks.push(`[FAIL] Docker not installed. Install from: https://docs.docker.com/get-docker/`);
return { content: [{ type: "text", text: checks.join("\n") }] };
}
const psResult = await commandRunner("docker ps");
if (psResult.success) {
checks.push(`[OK] Docker daemon is running`);
} else {
checks.push(`[FAIL] Docker daemon not running. Start with: sudo systemctl start docker`);
}
const composeResult = await commandRunner("docker compose version");
if (composeResult.success) {
checks.push(`[OK] Docker Compose: ${composeResult.stdout}`);
} else {
checks.push(`[WARN] Docker Compose not found (optional)`);
}
const whoamiResult = await commandRunner("whoami");
const groupResult = await commandRunner("groups");
if (groupResult.success && groupResult.stdout.includes("docker")) {
checks.push(`[OK] User is in docker group`);
} else {
checks.push(`[WARN] User not in docker group. May need sudo. Fix with:\n sudo usermod -aG docker ${whoamiResult.stdout}\n Then log out and back in.`);
}
return { content: [{ type: "text", text: checks.join("\n") }] };
}
export async function dockerAnalyzeProject({ path }) {
const projectPath = path || ".";
let projectType = "unknown";
const checks = [
{ file: "pom.xml", type: "java-maven" },
{ file: "build.gradle", type: "java-gradle" },
{ file: "package.json", type: "nodejs" },
{ file: "requirements.txt", type: "python" },
{ file: "go.mod", type: "golang" },
{ file: "Cargo.toml", type: "rust" },
];
for (const check of checks) {
try {
await access(`${projectPath}/${check.file}`, constants.F_OK);
projectType = check.type;
break;
} catch {}
}
const dockerfiles = {
"java-maven": `FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]`,
"nodejs": `FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]`,
"python": `FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]`,
"unknown": `# Could not detect project type
FROM ubuntu:22.04
WORKDIR /app
COPY . .`
};
const dockerfile = dockerfiles[projectType] || dockerfiles["unknown"];
return {
content: [{
type: "text",
text: `PROJECT TYPE DETECTED: ${projectType}\n\nRECOMMENDED DOCKERFILE:\n=======================\n${dockerfile}`
}]
};
}
export async function dockerBuild({ image_name, tag, path }) {
if (!/^[a-z0-9][a-z0-9._-]*$/.test(image_name)) {
return {
content: [{
type: "text",
text: `Invalid image name: "${image_name}"\n\nRules:\n- Must be lowercase\n- Must start with letter or number\n- Can contain: a-z, 0-9, ., -, _`
}]
};
}
try {
await access(`${path}/Dockerfile`, constants.F_OK);
} catch {
return {
content: [{
type: "text",
text: `No Dockerfile found in ${path}\n\nUse 'docker_analyze_project' tool to generate one.`
}]
};
}
const fullTag = `${image_name}:${tag}`;
const result = await commandRunner(`docker build -t ${fullTag} ${path}`, { timeout: 300000 });
if (!result.success) {
return {
content: [{
type: "text",
text: `Build failed!\n\nError:\n${result.stderr || result.error}`
}]
};
}
return {
content: [{
type: "text",
text: `Successfully built: ${fullTag}\n\nNEXT STEPS:\n1. Test locally: docker run -p 8080:8080 ${fullTag}`
}]
};
}
// ============================================
// GITHUB SECRETS TOOLS
// ============================================
export async function githubSecretsList({ repo }) {
const repoFlag = repo ? `-R ${repo}` : "";
const repoSecrets = await commandRunner(`gh secret list ${repoFlag}`);
let output = "REPOSITORY SECRETS\n==================\n";
if (repoSecrets.success && repoSecrets.stdout) {
output += repoSecrets.stdout + "\n";
} else if (repoSecrets.success) {
output += "No repository secrets configured.\n";
} else {
output += `Error: ${repoSecrets.stderr || repoSecrets.error}\n`;
output += "\nMake sure you're authenticated: gh auth login\n";
}
return { content: [{ type: "text", text: output }] };
}
export async function githubSecretsSet({ name, value, repo, env }) {
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {
return {
content: [{
type: "text",
text: `Invalid secret name: "${name}"\n\nRules:\n- Must be UPPERCASE\n- Must start with a letter\n- Can only contain A-Z, 0-9, and _`
}]
};
}
const maskedValue = value.substring(0, 3) + "***" + value.substring(value.length - 3);
const repoFlag = repo ? `-R ${repo}` : "";
const envFlag = env ? `--env ${env}` : "";
const result = await commandRunner(
`echo "${value.replace(/"/g, '\\"')}" | gh secret set ${name} ${repoFlag} ${envFlag}`,
{ timeout: 30000 }
);
if (!result.success) {
return {
content: [{
type: "text",
text: `Failed to set secret!\n\nError: ${result.stderr || result.error}`
}]
};
}
const location = env ? `environment "${env}"` : "repository";
return {
content: [{
type: "text",
text: `Secret set successfully!\n\nName: ${name}\nValue: ${maskedValue}\nLocation: ${location}`
}]
};
}
// ============================================
// AZURE TOOLS
// ============================================
export async function azureCheckCli() {
const checks = [];
const azVersion = await commandRunner("az --version | head -1");
if (azVersion.success) {
checks.push(`[OK] Azure CLI: ${azVersion.stdout}`);
} else {
checks.push(`[FAIL] Azure CLI not installed\n Install from: https://docs.microsoft.com/cli/azure/install-azure-cli`);
return { content: [{ type: "text", text: checks.join("\n\n") }] };
}
const account = await commandRunner("az account show --query '{name:name, id:id}' -o tsv");
if (account.success) {
checks.push(`[OK] Logged in to Azure\n Account: ${account.stdout.split("\t")[0]}`);
} else {
checks.push(`[FAIL] Not logged in to Azure\n Run: az login`);
}
return { content: [{ type: "text", text: checks.join("\n\n") }] };
}
export async function azureAcrSetup({ name, resource_group, location, sku }) {
if (!/^[a-zA-Z0-9]{5,50}$/.test(name)) {
return {
content: [{
type: "text",
text: `Invalid registry name: "${name}"\n\nRules:\n- 5-50 characters\n- Alphanumeric only (no hyphens or underscores)\n- Must be globally unique`
}]
};
}
// Check if resource group exists
const rgCheck = await commandRunner(`az group show -n ${resource_group} 2>/dev/null`);
let output = "";
if (!rgCheck.success) {
output += `Creating resource group: ${resource_group}...\n`;
const rgCreate = await commandRunner(`az group create -n ${resource_group} -l ${location}`);
if (!rgCreate.success) {
return {
content: [{
type: "text",
text: `Failed to create resource group!\n\nError: ${rgCreate.stderr || rgCreate.error}`
}]
};
}
output += `Resource group created.\n\n`;
}
output += `Creating container registry: ${name}...\n`;
const acrCreate = await commandRunner(
`az acr create -n ${name} -g ${resource_group} --sku ${sku} --admin-enabled true`,
{ timeout: 120000 }
);
if (!acrCreate.success) {
return {
content: [{
type: "text",
text: `Failed to create registry!\n\nError: ${acrCreate.stderr || acrCreate.error}`
}]
};
}
output += `\nRegistry created successfully!\n\nName: ${name}\nResource Group: ${resource_group}`;
return { content: [{ type: "text", text: output }] };
}
export async function azureContainerAppsDeploy({ app_name, resource_group, image, environment, port, cpu, memory }) {
const envName = environment || `${app_name}-env`;
let output = "";
const envCheck = await commandRunner(`az containerapp env show -n ${envName} -g ${resource_group} 2>/dev/null`);
if (!envCheck.success) {
output += `Creating Container Apps environment: ${envName}...\n`;
const envCreate = await commandRunner(
`az containerapp env create -n ${envName} -g ${resource_group}`,
{ timeout: 300000 }
);
if (!envCreate.success) {
return {
content: [{
type: "text",
text: `Failed to create environment!\n\nError: ${envCreate.stderr || envCreate.error}`
}]
};
}
output += `Environment created.\n\n`;
}
output += `Deploying container app: ${app_name}...\n`;
const isAcr = image.includes(".azurecr.io");
let registryArgs = "";
if (isAcr) {
const acrName = image.split(".")[0];
registryArgs = `--registry-server ${acrName}.azurecr.io`;
}
const deployCmd = `az containerapp create -n ${app_name} -g ${resource_group} --environment ${envName} --image ${image} --target-port ${port} --ingress external --cpu ${cpu} --memory ${memory} ${registryArgs}`;
const deploy = await commandRunner(deployCmd, { timeout: 300000 });
if (!deploy.success) {
return {
content: [{
type: "text",
text: `Failed to deploy!\n\nError: ${deploy.stderr || deploy.error}`
}]
};
}
const appUrl = await commandRunner(`az containerapp show -n ${app_name} -g ${resource_group} --query properties.configuration.ingress.fqdn -o tsv`);
output += `\nDeployment successful!\n\nApp: ${app_name}\nURL: https://${appUrl.stdout}`;
return { content: [{ type: "text", text: output }] };
}
// ============================================
// SONARCLOUD TOOLS
// ============================================
export function sonarcloudSetupGuide() {
return {
content: [{
type: "text",
text: `SONARCLOUD SETUP GUIDE
======================
STEP 1: Create SonarCloud Account
----------------------------------
1. Go to https://sonarcloud.io
2. Click "Log in" → "GitHub"
3. Authorize SonarCloud to access your GitHub
STEP 2: Import Your Project
----------------------------
1. Click "+ Create new project"
2. Select "Import from GitHub"
3. Choose your organization and repository
STEP 3: Generate Token
----------------------
1. Go to: https://sonarcloud.io/account/security
2. Generate a new token
3. Copy the token immediately!
STEP 4: Add GitHub Secret
-------------------------
gh secret set SONAR_TOKEN`
}]
};
}
export function sonarcloudCreateConfig({ organization, project_key, project_name, project_type, source_dir }) {
const configs = {
"java-maven": `sonar.organization=${organization}
sonar.projectKey=${project_key}
sonar.projectName=${project_name}
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes`,
"nodejs": `sonar.organization=${organization}
sonar.projectKey=${project_key}
sonar.projectName=${project_name}
sonar.sources=${source_dir || "src"}
sonar.javascript.lcov.reportPaths=coverage/lcov.info`,
"python": `sonar.organization=${organization}
sonar.projectKey=${project_key}
sonar.projectName=${project_name}
sonar.sources=${source_dir || "src"}
sonar.python.coverage.reportPaths=coverage.xml`
};
const config = configs[project_type];
if (!config) {
return {
content: [{
type: "text",
text: `Unknown project type: ${project_type}\n\nSupported: java-maven, java-gradle, nodejs, python, golang`
}]
};
}
return {
content: [{
type: "text",
text: `SONAR-PROJECT.PROPERTIES\n========================\n\nSave this to: ./sonar-project.properties\n\n${config}`
}]
};
}
// ============================================
// ONBOARDING TOOL
// ============================================
export async function devOnboardingCheck() {
const checks = [];
const gitVersion = await commandRunner("git --version");
checks.push(gitVersion.success ? `[OK] ${gitVersion.stdout}` : "[FAIL] Git not installed");
const gitConfig = await commandRunner("git config user.name && git config user.email");
checks.push(gitConfig.success ? `[OK] Git configured: ${gitConfig.stdout.replace("\n", " / ")}` : "[WARN] Git user not configured");
const ghVersion = await commandRunner("gh --version");
checks.push(ghVersion.success ? `[OK] GitHub CLI: ${ghVersion.stdout.split("\n")[0]}` : "[WARN] GitHub CLI not installed");
const dockerVersion = await commandRunner("docker --version");
checks.push(dockerVersion.success ? `[OK] ${dockerVersion.stdout}` : "[FAIL] Docker not installed");
const failCount = checks.filter(c => c.startsWith("[FAIL]")).length;
const warnCount = checks.filter(c => c.startsWith("[WARN]")).length;
let summary = "\n\n";
if (failCount === 0 && warnCount === 0) {
summary += "All checks passed! Developer environment is ready.";
} else {
summary += `Found ${failCount} critical issues and ${warnCount} warnings.`;
}
return {
content: [{
type: "text",
text: `DEVELOPER ENVIRONMENT CHECK\n============================\n\n${checks.join("\n\n")}${summary}`
}]
};
}
// Export all tools
export const tools = {
// Git
gitStatusExplained,
gitBranchExplained,
gitCommitGuided,
// Docker
dockerCheckSetup,
dockerAnalyzeProject,
dockerBuild,
// GitHub
githubSecretsList,
githubSecretsSet,
// Azure
azureCheckCli,
azureAcrSetup,
azureContainerAppsDeploy,
// SonarCloud
sonarcloudSetupGuide,
sonarcloudCreateConfig,
// Onboarding
devOnboardingCheck,
};