Skip to content

Commit a3ea27a

Browse files
author
Maxim Titovich
committed
feat: Improve file content retrieval and error handling in Azure DevOps service
- Add robust error handling for file content retrieval - Implement safe JSON stringification to prevent circular reference issues - Add binary file detection and handling - Enhance fallback mechanisms for file content retrieval - Update README with detailed file content tool documentation - Bump version to 1.0.4
1 parent 2a81abc commit a3ea27a

File tree

5 files changed

+268
-32
lines changed

5 files changed

+268
-32
lines changed

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,22 @@ registerTools(server, azureDevOpsService);
220220
| `azure_devops_branch_file_content` | Get file content directly from a branch | `repositoryId` (string), `branchName` (string), `filePath` (string), `startPosition` (number), `length` (number), `project` (string) |
221221
| `azure_devops_create_pr_comment` | Create a comment on a pull request | `repositoryId` (string), `pullRequestId` (number), `project` (string), `content` (string), and other optional parameters |
222222

223+
### File Content Tools
224+
225+
The file content tools (`azure_devops_pull_request_file_content` and `azure_devops_branch_file_content`) provide robust ways to access file content from repositories and pull requests:
226+
227+
- **Automatic fallback**: If direct object ID access fails, the system will try accessing by branch name
228+
- **Binary file detection**: Binary files are detected and handled appropriately
229+
- **Circular reference handling**: Prevents JSON serialization errors due to circular references
230+
- **Chunked access**: Large files can be accessed in chunks by specifying start position and length
231+
- **Error reporting**: Detailed error messages are provided when file access fails
232+
233+
When accessing large files or files in complex repositories, you may need to:
234+
235+
1. First try `azure_devops_pull_request_file_content` with the object ID from the PR changes
236+
2. If that fails, use `azure_devops_branch_file_content` with the branch name from the PR details
237+
3. For very large files, break down your requests into smaller chunks
238+
223239
## Development
224240

225241
### Prerequisites

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.3",
3+
"version": "1.0.4",
44
"description": "MCP Server for Cursor IDE-Azure DevOps Integration",
55
"main": "build/index.js",
66
"type": "module",

src/azure-devops-service.ts

+195-27
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,29 @@ import {
1212
PullRequestChanges,
1313
PullRequestCommentRequest,
1414
PullRequestCommentResponse,
15+
PullRequestFileContent,
1516
} from './types.js';
1617

18+
/**
19+
* Helper function to safely stringify objects with circular references
20+
*/
21+
function safeStringify(obj: any): string {
22+
const seen = new WeakSet();
23+
return JSON.stringify(obj, (key, value) => {
24+
// Skip _httpMessage, socket and similar properties that cause circular references
25+
if (key === '_httpMessage' || key === 'socket' || key === 'connection' || key === 'agent') {
26+
return '[Circular]';
27+
}
28+
if (typeof value === 'object' && value !== null) {
29+
if (seen.has(value)) {
30+
return '[Circular]';
31+
}
32+
seen.add(value);
33+
}
34+
return value;
35+
});
36+
}
37+
1738
/**
1839
* Service for interacting with Azure DevOps API
1940
*/
@@ -462,7 +483,7 @@ class AzureDevOpsService {
462483
startPosition: number,
463484
length: number,
464485
project?: string
465-
): Promise<{ content: string; size: number; position: number; length: number }> {
486+
): Promise<PullRequestFileContent> {
466487
await this.initialize();
467488

468489
if (!this.gitClient) {
@@ -477,20 +498,110 @@ class AzureDevOpsService {
477498
}
478499

479500
try {
501+
// Check if the file is binary first
502+
if (this.isBinaryFile(filePath)) {
503+
try {
504+
// Get metadata about the file to know its full size
505+
const item = await this.gitClient.getItem(repositoryId, filePath, projectName, objectId);
506+
507+
const fileSize = item?.contentMetadata?.contentLength || 0;
508+
509+
return {
510+
content: `[Binary file not displayed - ${Math.round(fileSize / 1024)}KB]`,
511+
size: fileSize,
512+
position: startPosition,
513+
length: 0,
514+
};
515+
} catch (error) {
516+
console.error(`Error getting binary file info: ${error}`);
517+
return {
518+
content: '[Binary file - Unable to retrieve size information]',
519+
size: 0,
520+
position: startPosition,
521+
length: 0,
522+
error: `Failed to get binary file info: ${(error as Error).message}`,
523+
};
524+
}
525+
}
526+
480527
// Get metadata about the file to know its full size
481528
const item = await this.gitClient.getItem(repositoryId, filePath, projectName, objectId);
482529

483530
const fileSize = item?.contentMetadata?.contentLength || 0;
484531

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-
);
532+
// Get the content - handle potential errors and circular references
533+
let rawContent;
534+
try {
535+
rawContent = await this.gitClient.getItemText(
536+
repositoryId,
537+
filePath,
538+
projectName,
539+
objectId,
540+
startPosition,
541+
length
542+
);
543+
} catch (textError) {
544+
console.error(`Error fetching file text: ${textError}`);
545+
// If direct text access fails, try using the branch approach
546+
try {
547+
// Get the PR details to find the source branch
548+
const pullRequest = await this.getPullRequestById(
549+
repositoryId,
550+
pullRequestId,
551+
projectName
552+
);
553+
554+
// Try the source branch first
555+
if (pullRequest.sourceRefName) {
556+
const branchResult = await this.getFileFromBranch(
557+
repositoryId,
558+
filePath,
559+
pullRequest.sourceRefName,
560+
startPosition,
561+
length,
562+
projectName
563+
);
564+
565+
if (!branchResult.error) {
566+
return branchResult;
567+
}
568+
}
569+
570+
// If source branch fails, try target branch
571+
if (pullRequest.targetRefName) {
572+
const branchResult = await this.getFileFromBranch(
573+
repositoryId,
574+
filePath,
575+
pullRequest.targetRefName,
576+
startPosition,
577+
length,
578+
projectName
579+
);
580+
581+
if (!branchResult.error) {
582+
return branchResult;
583+
}
584+
}
585+
586+
throw new Error('Failed to retrieve content using branch approach');
587+
} catch (branchError) {
588+
throw new Error(`Failed to retrieve content: ${(branchError as Error).message}`);
589+
}
590+
}
591+
592+
// Ensure content is a proper string
593+
let content = '';
594+
if (typeof rawContent === 'string') {
595+
content = rawContent;
596+
} else if (rawContent && typeof rawContent === 'object') {
597+
// If it's an object but not a string, try to convert it safely
598+
try {
599+
content = safeStringify(rawContent);
600+
} catch (stringifyError) {
601+
console.error(`Error stringifying content: ${stringifyError}`);
602+
content = '[Content could not be displayed due to format issues]';
603+
}
604+
}
494605

495606
return {
496607
content,
@@ -500,7 +611,13 @@ class AzureDevOpsService {
500611
};
501612
} catch (error) {
502613
console.error(`Error getting file content for ${filePath}:`, error);
503-
throw new Error(`Failed to retrieve content for file: ${filePath}`);
614+
return {
615+
content: '',
616+
size: 0,
617+
position: startPosition,
618+
length: 0,
619+
error: `Failed to retrieve content for file: ${filePath}. Error: ${(error as Error).message}`,
620+
};
504621
}
505622
}
506623

@@ -552,7 +669,7 @@ class AzureDevOpsService {
552669
startPosition = 0,
553670
length = 100000,
554671
project?: string
555-
): Promise<{ content: string; size: number; position: number; length: number; error?: string }> {
672+
): Promise<PullRequestFileContent> {
556673
await this.initialize();
557674

558675
if (!this.gitClient) {
@@ -601,22 +718,72 @@ class AzureDevOpsService {
601718

602719
const fileSize = item?.contentMetadata?.contentLength || 0;
603720

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-
},
721+
// Handle binary files
722+
if (this.isBinaryFile(filePath)) {
723+
return {
724+
content: `[Binary file not displayed - ${Math.round(fileSize / 1024)}KB]`,
725+
size: fileSize,
726+
position: startPosition,
727+
length: 0,
728+
error: undefined,
729+
};
730+
}
731+
732+
// Get the content - carefully handle the response to prevent circular references
733+
let rawContent;
734+
try {
735+
rawContent = await this.gitClient.getItemText(
736+
repositoryId,
737+
filePath,
738+
projectName,
739+
undefined,
740+
startPosition,
741+
length,
742+
{
743+
versionDescriptor: {
744+
version: commitId,
745+
versionOptions: 0, // Use exact version
746+
versionType: 1, // Commit
747+
},
748+
}
749+
);
750+
} catch (textError) {
751+
console.error(`Error fetching file text: ${textError}`);
752+
// If getItemText fails, try to get content as Buffer and convert it
753+
const contentBuffer = await this.gitClient.getItemContent(
754+
repositoryId,
755+
filePath,
756+
projectName,
757+
undefined,
758+
{
759+
versionDescriptor: {
760+
version: commitId,
761+
versionOptions: 0,
762+
versionType: 1,
763+
},
764+
}
765+
);
766+
767+
if (Buffer.isBuffer(contentBuffer)) {
768+
rawContent = contentBuffer.toString('utf8');
769+
} else {
770+
throw new Error('Failed to retrieve file content in any format');
618771
}
619-
);
772+
}
773+
774+
// Ensure content is a proper string
775+
let content = '';
776+
if (typeof rawContent === 'string') {
777+
content = rawContent;
778+
} else if (rawContent && typeof rawContent === 'object') {
779+
// If it's an object but not a string, try to convert it safely
780+
try {
781+
content = safeStringify(rawContent);
782+
} catch (stringifyError) {
783+
console.error(`Error stringifying content: ${stringifyError}`);
784+
content = '[Content could not be displayed due to format issues]';
785+
}
786+
}
620787

621788
return {
622789
content,
@@ -625,6 +792,7 @@ class AzureDevOpsService {
625792
length: content.length,
626793
};
627794
} catch (error) {
795+
console.error(`Error getting file from branch: ${error}`);
628796
return {
629797
content: '',
630798
size: 0,

src/index.ts

+54-4
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,61 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
55
import { z } from 'zod';
66
import { azureDevOpsService } from './azure-devops-service.js';
77
import { configManager } from './config-manager.js';
8+
import { createConnection } from 'net';
9+
10+
/**
11+
* Helper function to safely handle response serialization
12+
* preventing circular reference errors
13+
*/
14+
function safeResponse(result: any) {
15+
// If the result is already a string, return it
16+
if (typeof result === 'string') {
17+
return result;
18+
}
19+
20+
try {
21+
// Try to JSON stringify normally first
22+
return JSON.stringify(result, null, 2);
23+
} catch (error) {
24+
// If normal stringify fails, use a more robust approach
25+
const seen = new WeakSet();
26+
try {
27+
return JSON.stringify(
28+
result,
29+
(key, value) => {
30+
// Skip problematic keys that often cause circular references
31+
if (
32+
key === '_httpMessage' ||
33+
key === 'socket' ||
34+
key === 'connection' ||
35+
key === 'agent'
36+
) {
37+
return '[Circular]';
38+
}
39+
if (typeof value === 'object' && value !== null) {
40+
if (seen.has(value)) {
41+
return '[Circular]';
42+
}
43+
seen.add(value);
44+
}
45+
return value;
46+
},
47+
2
48+
);
49+
} catch (secondError) {
50+
// If all else fails, convert to a simple error message
51+
return JSON.stringify({
52+
error: 'Failed to serialize response',
53+
message: error instanceof Error ? error.message : 'Unknown error',
54+
});
55+
}
56+
}
57+
}
858

959
// Create MCP server
1060
const server = new McpServer({
1161
name: 'cursor-azure-devops-mcp',
12-
version: '1.0.0',
62+
version: '1.0.3',
1363
description: 'MCP Server for Azure DevOps integration with Cursor IDE',
1464
});
1565

@@ -274,7 +324,7 @@ server.tool(
274324
content: [
275325
{
276326
type: 'text',
277-
text: JSON.stringify(result, null, 2),
327+
text: safeResponse(result),
278328
},
279329
],
280330
};
@@ -309,7 +359,7 @@ server.tool(
309359
content: [
310360
{
311361
type: 'text',
312-
text: JSON.stringify(result, null, 2),
362+
text: safeResponse(result),
313363
},
314364
],
315365
};
@@ -342,7 +392,7 @@ server.tool(
342392
content: [
343393
{
344394
type: 'text',
345-
text: JSON.stringify(result, null, 2),
395+
text: safeResponse(result),
346396
},
347397
],
348398
};

0 commit comments

Comments
 (0)