Skip to content

Commit 2a81abc

Browse files
author
Maxim Titovich
committed
feat: Add support for retrieving file content from Azure DevOps branches
- Implement new `getFileFromBranch` method in Azure DevOps service - Add `azure_devops_branch_file_content` tool for accessing file contents directly from branches - Update README with new tool documentation - Create `PullRequestFileContent` type for consistent file retrieval response - Bump version to 1.0.3
1 parent 11d4008 commit 2a81abc

6 files changed

+189
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ registerTools(server, azureDevOpsService);
217217
| `azure_devops_work_item_attachments`| Get attachments for a work item | `id` (number) |
218218
| `azure_devops_pull_request_changes` | Get detailed PR code changes | `repositoryId` (string), `pullRequestId` (number), `project` (string) |
219219
| `azure_devops_pull_request_file_content` | Get content of large files in chunks | `repositoryId` (string), `pullRequestId` (number), `filePath` (string), `objectId` (string), `startPosition` (number), `length` (number), `project` (string) |
220+
| `azure_devops_branch_file_content` | Get file content directly from a branch | `repositoryId` (string), `branchName` (string), `filePath` (string), `startPosition` (number), `length` (number), `project` (string) |
220221
| `azure_devops_create_pr_comment` | Create a comment on a pull request | `repositoryId` (string), `pullRequestId` (number), `project` (string), `content` (string), and other optional parameters |
221222

222223
## Development

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cursor-azure-devops-mcp",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "MCP Server for Cursor IDE-Azure DevOps Integration",
55
"main": "build/index.js",
66
"type": "module",

src/azure-devops-service.ts

+104
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,110 @@ class AzureDevOpsService {
541541
return binaryExtensions.includes(extension);
542542
}
543543

544+
/**
545+
* Helper method to get a file's content from a specific branch
546+
* This is useful for accessing files in pull requests when direct object ID access fails
547+
*/
548+
async getFileFromBranch(
549+
repositoryId: string,
550+
filePath: string,
551+
branchName: string,
552+
startPosition = 0,
553+
length = 100000,
554+
project?: string
555+
): Promise<{ content: string; size: number; position: number; length: number; error?: string }> {
556+
await this.initialize();
557+
558+
if (!this.gitClient) {
559+
throw new Error('Git client not initialized');
560+
}
561+
562+
// Use the provided project or fall back to the default project
563+
const projectName = project || this.defaultProject;
564+
565+
if (!projectName) {
566+
throw new Error('Project name is required');
567+
}
568+
569+
try {
570+
// Clean branch name (remove refs/heads/ if present)
571+
const cleanBranchName = branchName.replace(/^refs\/heads\//, '');
572+
573+
// Get the branch reference to find the latest commit
574+
const refs = await this.gitClient.getRefs(
575+
repositoryId,
576+
projectName,
577+
`heads/${cleanBranchName}`
578+
);
579+
580+
if (!refs || refs.length === 0) {
581+
return {
582+
content: '',
583+
size: 0,
584+
position: startPosition,
585+
length: 0,
586+
error: `Branch reference not found for ${cleanBranchName}`,
587+
};
588+
}
589+
590+
const commitId = refs[0].objectId;
591+
592+
// Get metadata about the file to know its full size
593+
try {
594+
const item = await this.gitClient.getItem(repositoryId, filePath, projectName, undefined, {
595+
versionDescriptor: {
596+
version: commitId,
597+
versionOptions: 0, // Use exact version
598+
versionType: 1, // Commit
599+
},
600+
});
601+
602+
const fileSize = item?.contentMetadata?.contentLength || 0;
603+
604+
// Get the content
605+
const content = await this.gitClient.getItemText(
606+
repositoryId,
607+
filePath,
608+
projectName,
609+
undefined,
610+
startPosition,
611+
length,
612+
{
613+
versionDescriptor: {
614+
version: commitId,
615+
versionOptions: 0, // Use exact version
616+
versionType: 1, // Commit
617+
},
618+
}
619+
);
620+
621+
return {
622+
content,
623+
size: fileSize,
624+
position: startPosition,
625+
length: content.length,
626+
};
627+
} catch (error) {
628+
return {
629+
content: '',
630+
size: 0,
631+
position: startPosition,
632+
length: 0,
633+
error: `Failed to retrieve file from branch ${cleanBranchName}: ${(error as Error).message}`,
634+
};
635+
}
636+
} catch (error) {
637+
console.error(`Error accessing branch ${branchName}:`, error);
638+
return {
639+
content: '',
640+
size: 0,
641+
position: startPosition,
642+
length: 0,
643+
error: `Failed to access branch ${branchName}: ${(error as Error).message}`,
644+
};
645+
}
646+
}
647+
544648
/**
545649
* Create a comment on a pull request
546650
*/

src/index.ts

+36
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,39 @@ server.tool(
316316
}
317317
);
318318

319+
// New tool for getting file content directly from a branch
320+
server.tool(
321+
'azure_devops_branch_file_content',
322+
'Get content of a file directly from a branch (helps with PR file access)',
323+
{
324+
repositoryId: z.string().describe('Repository ID'),
325+
branchName: z.string().describe('Branch name'),
326+
filePath: z.string().describe('File path'),
327+
startPosition: z.number().describe('Starting position in the file (bytes)').default(0),
328+
length: z.number().describe('Length to read (bytes)').default(100000),
329+
project: z.string().describe('Project name'),
330+
},
331+
async ({ repositoryId, branchName, filePath, startPosition, length, project }) => {
332+
const result = await azureDevOpsService.getFileFromBranch(
333+
repositoryId,
334+
filePath,
335+
branchName,
336+
startPosition,
337+
length,
338+
project
339+
);
340+
341+
return {
342+
content: [
343+
{
344+
type: 'text',
345+
text: JSON.stringify(result, null, 2),
346+
},
347+
],
348+
};
349+
}
350+
);
351+
319352
// New tool for creating pull request comments
320353
server.tool(
321354
'azure_devops_create_pr_comment',
@@ -393,6 +426,9 @@ async function main() {
393426
console.error(
394427
'- azure_devops_pull_request_file_content: Get content of a specific file in chunks (for large files)'
395428
);
429+
console.error(
430+
'- azure_devops_branch_file_content: Get content of a file directly from a branch'
431+
);
396432
console.error('- azure_devops_create_pr_comment: Create a comment on a pull request');
397433
} catch (error) {
398434
console.error('Error starting server:', error);

src/sse-server.ts

+36
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,42 @@ app.get('/sse', async (req, res) => {
287287
}
288288
);
289289

290+
// New tool for getting file content directly from a branch
291+
server.tool(
292+
'azure_devops_branch_file_content',
293+
'Get content of a file directly from a branch (helps with PR file access)',
294+
{
295+
repositoryId: z.string().describe('Repository ID'),
296+
branchName: z.string().describe('Branch name'),
297+
filePath: z.string().describe('File path'),
298+
startPosition: z.number().describe('Starting position in the file (bytes)').default(0),
299+
length: z.number().describe('Length to read (bytes)').default(100000),
300+
project: z.string().describe('Project name'),
301+
},
302+
async (
303+
{ repositoryId, branchName, filePath, startPosition, length, project },
304+
{ signal }
305+
) => {
306+
signal.throwIfAborted();
307+
const result = await azureDevOpsService.getFileFromBranch(
308+
repositoryId,
309+
filePath,
310+
branchName,
311+
startPosition,
312+
length,
313+
project
314+
);
315+
return {
316+
content: [
317+
{
318+
type: 'text',
319+
text: JSON.stringify(result, null, 2),
320+
},
321+
],
322+
};
323+
}
324+
);
325+
290326
// New tool for creating pull request comments
291327
server.tool(
292328
'azure_devops_create_pr_comment',

src/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,14 @@ export interface PullRequestCommentResponse {
178178
createdDate?: string;
179179
url?: string;
180180
}
181+
182+
/**
183+
* Response type for file content retrieval in pull requests
184+
*/
185+
export interface PullRequestFileContent {
186+
content: string;
187+
size: number;
188+
position: number;
189+
length: number;
190+
error?: string;
191+
}

0 commit comments

Comments
 (0)