Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Interact with these Azure DevOps services:
- **build_get_builds**: Retrieves a list of builds for a given project.
- **build_get_log**: Retrieves the logs for a specific build.
- **build_get_log_by_id**: Get a specific build log by log ID.
- **build_get_logs_zip**: Downloads build logs as ZIP, extracts with nested archive support creates analysis guide, and opens in VS Code for comprehensive build failure investigation.
- **build_get_changes**: Get the changes associated with a specific build.
- **build_run_build**: Triggers a new build for a specified definition.
- **build_get_status**: Fetches the status of a specific build.
Expand Down
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"azure-devops-extension-sdk": "^4.0.2",
"azure-devops-node-api": "^15.1.0",
"zod": "^3.25.63",
"zod-to-json-schema": "^3.24.5"
"zod-to-json-schema": "^3.24.5",
"adm-zip": "^0.5.16"
},
"devDependencies": {
"@modelcontextprotocol/inspector": "^0.15.0",
Expand All @@ -50,6 +51,7 @@
"ts-jest": "^29.4.0",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1"
"typescript-eslint": "^8.32.1",
"@types/adm-zip": "^0.5.7"
}
}
63 changes: 63 additions & 0 deletions src/tools/builds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebApi } from "azure-devops-node-api";
import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
import { z } from "zod";
import * as fs from "fs";
import AdmZip from "adm-zip";
import { streamToBuffer, createLogPaths, ensureDownloadsDirectory, extractNestedZips, createAnalysisPrompt, openInVSCode, cleanupZipFile } from "../utils.js";

const BUILD_TOOLS = {
get_definitions: "build_get_definitions",
get_definition_revisions: "build_get_definition_revisions",
get_builds: "build_get_builds",
get_log: "build_get_log",
get_log_by_id: "build_get_log_by_id",
get_logs_zip: "build_get_logs_zip",
get_changes: "build_get_changes",
run_build: "build_run_build",
get_status: "build_get_status"
Expand Down Expand Up @@ -327,6 +331,65 @@ function configureBuildTools(
};
}
);

server.tool(
BUILD_TOOLS.get_logs_zip,
"Downloads build logs as ZIP, extracts with nested archive support, creates analysis guide, and opens in VS Code for comprehensive build failure investigation.",
{
project: z.string().describe("Project ID or name to get the build logs for"),
buildId: z.number().describe("ID of the build to get the logs ZIP for"),
},
async ({ project, buildId }) => {
const connection = await connectionProvider();
const buildApi = await connection.getBuildApi();
const logsZip = await buildApi.getBuildLogsZip(project, buildId);

// Convert the stream to buffer
const buffer = await streamToBuffer(logsZip);

// Create paths for ZIP and extraction
const { filename, folderName, zipFilePath, extractDir } = createLogPaths(buildId);

// Ensure downloads directory exists
ensureDownloadsDirectory();

// Write the ZIP file to downloads directory
fs.writeFileSync(zipFilePath, buffer);

// Extract the ZIP file
const zip = new AdmZip(zipFilePath);
zip.extractAllTo(extractDir, true);

// Recursively extract any nested ZIP files
extractNestedZips(extractDir);

// Create analysis prompt file
createAnalysisPrompt(extractDir, project, buildId);

// Open the extracted folder in VS Code
openInVSCode(extractDir);

// Clean up original ZIP file
cleanupZipFile(zipFilePath);

return {
content: [
{
type: "text",
text: JSON.stringify({
buildId,
project,
zipSizeBytes: buffer.length,
extractedPath: extractDir,
folderName,
message: `Build logs extracted and opened in VS Code at: ${extractDir}`
}, null, 2)
}
],
};
}
);

}

export { BUILD_TOOLS, configureBuildTools };
123 changes: 123 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,130 @@
// Licensed under the MIT License.

import { packageVersion } from "./version.js";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import * as child_process from "child_process";
import AdmZip from "adm-zip";

export const apiVersion = "7.2-preview.1";
export const batchApiVersion = "5.0";
export const userAgent = `AzureDevOps.MCP/${packageVersion} (local)`


// Helper function to convert stream to buffer
export async function streamToBuffer(stream: NodeJS.ReadableStream): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks);
}

// Helper function to create unique filename and paths
export function createLogPaths(buildId: number): { filename: string; folderName: string; zipFilePath: string; extractDir: string } {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `build-${buildId}-logs-${timestamp}.zip`;
const folderName = `build-${buildId}-logs-${timestamp}`;
const downloadsDir = path.join(os.homedir(), 'Downloads');
const zipFilePath = path.join(downloadsDir, filename);
const extractDir = path.join(downloadsDir, folderName);

return { filename, folderName, zipFilePath, extractDir };
}

// Helper function to ensure downloads directory exists
export function ensureDownloadsDirectory(): string {
const downloadsDir = path.join(os.homedir(), 'Downloads');
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
return downloadsDir;
}

// Recursive function to extract nested ZIP files
export function extractNestedZips(dir: string): void {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);

if (stat.isDirectory()) {
// Recursively process subdirectories
extractNestedZips(filePath);
} else if (file.toLowerCase().endsWith('.zip')) {
try {
// Extract nested ZIP file
const nestedZip = new AdmZip(filePath);
const nestedExtractDir = path.join(dir, file.replace(/\.zip$/i, ''));

// Create directory for nested extraction
if (!fs.existsSync(nestedExtractDir)) {
fs.mkdirSync(nestedExtractDir, { recursive: true });
}

// Extract nested ZIP
nestedZip.extractAllTo(nestedExtractDir, true);

// Remove the original ZIP file after extraction
fs.unlinkSync(filePath);

// Recursively check the newly extracted directory for more ZIPs
extractNestedZips(nestedExtractDir);
} catch (error) {
console.warn(`Failed to extract nested ZIP file ${filePath}:`, error);
}
}
}
}

// Helper function to create analysis prompt file
export function createAnalysisPrompt(extractDir: string, project: string, buildId: number): void {
const promptFile = path.join(extractDir, 'ANALYSIS_PROMPT.txt');
const promptContent = `BUILD LOG ANALYSIS GUIDE
========================
Build Information:
- Project: ${project}
- Build ID: ${buildId}
- Extracted: ${new Date().toISOString()}
Analysis Tasks:
1. Look for ERROR, FAILED, or EXCEPTION keywords in log files
2. Check pipeline YAML files for configuration issues
3. Examine test results and failure reports
4. Review dependency installation logs
5. Identify the exact failure point and error messages
Common File Types to Check:
- *.log - Build execution logs
- *.yml/*.yaml - Pipeline configuration
- *.xml - Test results (MSTest, NUnit, etc.)
- *.trx - Visual Studio test results
- *.json - Package.json, build configs
- **/logs/** - Nested log directories
Search Strategy:
Use VS Code search (Ctrl+Shift+F) to find:
- "error" (case insensitive)
- "failed" (case insensitive)
- "exception" (case insensitive)
- "##[error]" (Azure DevOps error marker)
- Exit codes: "exit code 1", "returned 1"
`;
fs.writeFileSync(promptFile, promptContent);
}

// Helper function to open directory in VS Code
export function openInVSCode(extractDir: string): void {
child_process.exec(`code "${extractDir}"`, (error) => {
if (error) {
console.warn('Could not open VS Code automatically. Please open the folder manually:', extractDir);
}
});
}

// Helper function to clean up ZIP file
export function cleanupZipFile(zipFilePath: string): void {
try {
fs.unlinkSync(zipFilePath);
} catch (error) {
console.warn('Could not remove original ZIP file:', error);
}
}
Loading