Skip to content

Commit 11d4008

Browse files
author
Maxim Titovich
committed
feat: Add support for retrieving large file contents in pull requests
- Enhance Azure DevOps service to handle large files in pull request changes - Implement chunked file content retrieval with size and preview metadata - Add new tool `azure_devops_pull_request_file_content` for fetching file contents - Update types to include additional file metadata - Bump version to 1.0.2
1 parent 9bbab26 commit 11d4008

File tree

6 files changed

+221
-22
lines changed

6 files changed

+221
-22
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ registerTools(server, azureDevOpsService);
216216
| `azure_devops_pull_request_threads` | Get threads from a pull request | `repositoryId` (string), `pullRequestId` (number), `project` (string) |
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) |
219+
| `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) |
219220
| `azure_devops_create_pr_comment` | Create a comment on a pull request | `repositoryId` (string), `pullRequestId` (number), `project` (string), `content` (string), and other optional parameters |
220221

221222
## Development

package.json

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

src/azure-devops-service.ts

Lines changed: 134 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,21 @@ class AzureDevOpsService {
305305
projectName
306306
);
307307

308-
// Get detailed content for each change (with size limits for safety)
309-
const MAX_FILE_SIZE = 100000; // Limit file size to 100KB for performance
308+
// File size and content handling constants
309+
const MAX_INLINE_FILE_SIZE = 500000; // Increased to 500KB for inline content
310+
const MAX_CHUNK_SIZE = 100000; // 100KB chunks for larger files
311+
const PREVIEW_SIZE = 10000; // 10KB preview for very large files
312+
313+
// Get detailed content for each change
310314
const enhancedChanges = await Promise.all(
311315
(changes.changeEntries || []).map(async (change: any) => {
312316
const filePath = change.item?.path || '';
313317
let originalContent = null;
314318
let modifiedContent = null;
319+
let originalContentSize = 0;
320+
let modifiedContentSize = 0;
321+
let originalContentPreview = null;
322+
let modifiedContentPreview = null;
315323

316324
// Skip folders or binary files
317325
const isBinary = this.isBinaryFile(filePath);
@@ -322,21 +330,44 @@ class AzureDevOpsService {
322330
// Get original content if the file wasn't newly added
323331
if (change.changeType !== 'add' && change.originalObjectId) {
324332
try {
325-
const originalItemContent = await this.gitClient.getItemContent(
333+
// First get the item metadata to check file size
334+
const originalItem = await this.gitClient.getItem(
326335
repositoryId,
327336
filePath,
328337
projectName,
329-
change.originalObjectId,
330-
undefined,
331-
true,
332-
true
338+
change.originalObjectId
333339
);
334340

335-
// Check if the content is too large
336-
if (originalItemContent && originalItemContent.length < MAX_FILE_SIZE) {
341+
originalContentSize = originalItem?.contentMetadata?.contentLength || 0;
342+
343+
// For files within the inline limit, get full content
344+
if (originalContentSize <= MAX_INLINE_FILE_SIZE) {
345+
const originalItemContent = await this.gitClient.getItemContent(
346+
repositoryId,
347+
filePath,
348+
projectName,
349+
change.originalObjectId,
350+
undefined,
351+
true,
352+
true
353+
);
354+
337355
originalContent = originalItemContent.toString('utf8');
338-
} else {
339-
originalContent = '(File too large to display)';
356+
}
357+
// For large files, get a preview
358+
else {
359+
// Get just the beginning of the file for preview
360+
const previewContent = await this.gitClient.getItemText(
361+
repositoryId,
362+
filePath,
363+
projectName,
364+
change.originalObjectId,
365+
0, // Start at beginning
366+
PREVIEW_SIZE // Get preview bytes
367+
);
368+
369+
originalContentPreview = previewContent;
370+
originalContent = `(File too large to display inline - ${Math.round(originalContentSize / 1024)}KB. Preview shown.)`;
340371
}
341372
} catch (error) {
342373
console.error(`Error getting original content for ${filePath}:`, error);
@@ -347,21 +378,44 @@ class AzureDevOpsService {
347378
// Get modified content if the file wasn't deleted
348379
if (change.changeType !== 'delete' && change.item.objectId) {
349380
try {
350-
const modifiedItemContent = await this.gitClient.getItemContent(
381+
// First get the item metadata to check file size
382+
const modifiedItem = await this.gitClient.getItem(
351383
repositoryId,
352384
filePath,
353385
projectName,
354-
change.item.objectId,
355-
undefined,
356-
true,
357-
true
386+
change.item.objectId
358387
);
359388

360-
// Check if the content is too large
361-
if (modifiedItemContent && modifiedItemContent.length < MAX_FILE_SIZE) {
389+
modifiedContentSize = modifiedItem?.contentMetadata?.contentLength || 0;
390+
391+
// For files within the inline limit, get full content
392+
if (modifiedContentSize <= MAX_INLINE_FILE_SIZE) {
393+
const modifiedItemContent = await this.gitClient.getItemContent(
394+
repositoryId,
395+
filePath,
396+
projectName,
397+
change.item.objectId,
398+
undefined,
399+
true,
400+
true
401+
);
402+
362403
modifiedContent = modifiedItemContent.toString('utf8');
363-
} else {
364-
modifiedContent = '(File too large to display)';
404+
}
405+
// For large files, get a preview
406+
else {
407+
// Get just the beginning of the file for preview
408+
const previewContent = await this.gitClient.getItemText(
409+
repositoryId,
410+
filePath,
411+
projectName,
412+
change.item.objectId,
413+
0, // Start at beginning
414+
PREVIEW_SIZE // Get preview bytes
415+
);
416+
417+
modifiedContentPreview = previewContent;
418+
modifiedContent = `(File too large to display inline - ${Math.round(modifiedContentSize / 1024)}KB. Preview shown.)`;
365419
}
366420
} catch (error) {
367421
console.error(`Error getting modified content for ${filePath}:`, error);
@@ -378,6 +432,12 @@ class AzureDevOpsService {
378432
...change,
379433
originalContent,
380434
modifiedContent,
435+
originalContentSize,
436+
modifiedContentSize,
437+
originalContentPreview,
438+
modifiedContentPreview,
439+
isBinary,
440+
isFolder,
381441
} as PullRequestChange;
382442

383443
return enhancedChange;
@@ -390,6 +450,60 @@ class AzureDevOpsService {
390450
};
391451
}
392452

453+
/**
454+
* Get content for a specific file in a pull request by chunks
455+
* This allows retrieving parts of large files that can't be displayed inline
456+
*/
457+
async getPullRequestFileContent(
458+
repositoryId: string,
459+
pullRequestId: number,
460+
filePath: string,
461+
objectId: string,
462+
startPosition: number,
463+
length: number,
464+
project?: string
465+
): Promise<{ content: string; size: number; position: number; length: number }> {
466+
await this.initialize();
467+
468+
if (!this.gitClient) {
469+
throw new Error('Git client not initialized');
470+
}
471+
472+
// Use the provided project or fall back to the default project
473+
const projectName = project || this.defaultProject;
474+
475+
if (!projectName) {
476+
throw new Error('Project name is required');
477+
}
478+
479+
try {
480+
// Get metadata about the file to know its full size
481+
const item = await this.gitClient.getItem(repositoryId, filePath, projectName, objectId);
482+
483+
const fileSize = item?.contentMetadata?.contentLength || 0;
484+
485+
// Get the specified chunk of content
486+
const content = await this.gitClient.getItemText(
487+
repositoryId,
488+
filePath,
489+
projectName,
490+
objectId,
491+
startPosition,
492+
length
493+
);
494+
495+
return {
496+
content,
497+
size: fileSize,
498+
position: startPosition,
499+
length: content.length,
500+
};
501+
} catch (error) {
502+
console.error(`Error getting file content for ${filePath}:`, error);
503+
throw new Error(`Failed to retrieve content for file: ${filePath}`);
504+
}
505+
}
506+
393507
/**
394508
* Helper function to determine if a file is likely binary based on extension
395509
*/

src/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,42 @@ server.tool(
269269
pullRequestId,
270270
project
271271
);
272+
273+
return {
274+
content: [
275+
{
276+
type: 'text',
277+
text: JSON.stringify(result, null, 2),
278+
},
279+
],
280+
};
281+
}
282+
);
283+
284+
// New tool for getting content of large files in pull requests by chunks
285+
server.tool(
286+
'azure_devops_pull_request_file_content',
287+
'Get content of a specific file in a pull request by chunks (for large files)',
288+
{
289+
repositoryId: z.string().describe('Repository ID'),
290+
pullRequestId: z.number().describe('Pull request ID'),
291+
filePath: z.string().describe('File path'),
292+
objectId: z.string().describe('Object ID of the file version'),
293+
startPosition: z.number().describe('Starting position in the file (bytes)'),
294+
length: z.number().describe('Length to read (bytes)'),
295+
project: z.string().describe('Project name'),
296+
},
297+
async ({ repositoryId, pullRequestId, filePath, objectId, startPosition, length, project }) => {
298+
const result = await azureDevOpsService.getPullRequestFileContent(
299+
repositoryId,
300+
pullRequestId,
301+
filePath,
302+
objectId,
303+
startPosition,
304+
length,
305+
project
306+
);
307+
272308
return {
273309
content: [
274310
{
@@ -354,6 +390,9 @@ async function main() {
354390
console.error(
355391
'- azure_devops_pull_request_changes: Get detailed code changes for a pull request'
356392
);
393+
console.error(
394+
'- azure_devops_pull_request_file_content: Get content of a specific file in chunks (for large files)'
395+
);
357396
console.error('- azure_devops_create_pr_comment: Create a comment on a pull request');
358397
} catch (error) {
359398
console.error('Error starting server:', error);

src/sse-server.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,8 @@ app.get('/sse', async (req, res) => {
231231
pullRequestId: z.number().describe('Pull request ID'),
232232
project: z.string().describe('Project name'),
233233
},
234-
async ({ repositoryId, pullRequestId, project }) => {
234+
async ({ repositoryId, pullRequestId, project }, { signal }) => {
235+
signal.throwIfAborted();
235236
const result = await azureDevOpsService.getPullRequestChanges(
236237
repositoryId,
237238
pullRequestId,
@@ -248,6 +249,44 @@ app.get('/sse', async (req, res) => {
248249
}
249250
);
250251

252+
// New tool for getting content of large files in pull requests by chunks
253+
server.tool(
254+
'azure_devops_pull_request_file_content',
255+
'Get content of a specific file in a pull request by chunks (for large files)',
256+
{
257+
repositoryId: z.string().describe('Repository ID'),
258+
pullRequestId: z.number().describe('Pull request ID'),
259+
filePath: z.string().describe('File path'),
260+
objectId: z.string().describe('Object ID of the file version'),
261+
startPosition: z.number().describe('Starting position in the file (bytes)'),
262+
length: z.number().describe('Length to read (bytes)'),
263+
project: z.string().describe('Project name'),
264+
},
265+
async (
266+
{ repositoryId, pullRequestId, filePath, objectId, startPosition, length, project },
267+
{ signal }
268+
) => {
269+
signal.throwIfAborted();
270+
const result = await azureDevOpsService.getPullRequestFileContent(
271+
repositoryId,
272+
pullRequestId,
273+
filePath,
274+
objectId,
275+
startPosition,
276+
length,
277+
project
278+
);
279+
return {
280+
content: [
281+
{
282+
type: 'text',
283+
text: JSON.stringify(result, null, 2),
284+
},
285+
],
286+
};
287+
}
288+
);
289+
251290
// New tool for creating pull request comments
252291
server.tool(
253292
'azure_devops_create_pr_comment',

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ export interface PullRequestChange {
140140
changeType?: string; // Add, Edit, Delete
141141
originalContent?: string; // Content before change
142142
modifiedContent?: string; // Content after change
143+
originalContentSize?: number; // Size of original file in bytes
144+
modifiedContentSize?: number; // Size of modified file in bytes
145+
originalContentPreview?: string; // Preview of content for large files
146+
modifiedContentPreview?: string; // Preview of content for large files
147+
isBinary?: boolean; // Whether the file is binary
148+
isFolder?: boolean; // Whether the item is a folder
143149
}
144150

145151
export interface PullRequestChanges {

0 commit comments

Comments
 (0)