cursor-azure-devops-mcp
Advanced tools
Comparing version
@@ -213,4 +213,7 @@ import * as azdev from 'azure-devops-node-api'; | ||
projectName); | ||
// Get detailed content for each change (with size limits for safety) | ||
const MAX_FILE_SIZE = 100000; // Limit file size to 100KB for performance | ||
// File size and content handling constants | ||
const MAX_INLINE_FILE_SIZE = 500000; // Increased to 500KB for inline content | ||
const MAX_CHUNK_SIZE = 100000; // 100KB chunks for larger files | ||
const PREVIEW_SIZE = 10000; // 10KB preview for very large files | ||
// Get detailed content for each change | ||
const enhancedChanges = await Promise.all((changes.changeEntries || []).map(async (change) => { | ||
@@ -220,2 +223,6 @@ const filePath = change.item?.path || ''; | ||
let modifiedContent = null; | ||
let originalContentSize = 0; | ||
let modifiedContentSize = 0; | ||
let originalContentPreview = null; | ||
let modifiedContentPreview = null; | ||
// Skip folders or binary files | ||
@@ -229,9 +236,18 @@ const isBinary = this.isBinaryFile(filePath); | ||
try { | ||
const originalItemContent = await this.gitClient.getItemContent(repositoryId, filePath, projectName, change.originalObjectId, undefined, true, true); | ||
// Check if the content is too large | ||
if (originalItemContent && originalItemContent.length < MAX_FILE_SIZE) { | ||
// First get the item metadata to check file size | ||
const originalItem = await this.gitClient.getItem(repositoryId, filePath, projectName, change.originalObjectId); | ||
originalContentSize = originalItem?.contentMetadata?.contentLength || 0; | ||
// For files within the inline limit, get full content | ||
if (originalContentSize <= MAX_INLINE_FILE_SIZE) { | ||
const originalItemContent = await this.gitClient.getItemContent(repositoryId, filePath, projectName, change.originalObjectId, undefined, true, true); | ||
originalContent = originalItemContent.toString('utf8'); | ||
} | ||
// For large files, get a preview | ||
else { | ||
originalContent = '(File too large to display)'; | ||
// Get just the beginning of the file for preview | ||
const previewContent = await this.gitClient.getItemText(repositoryId, filePath, projectName, change.originalObjectId, 0, // Start at beginning | ||
PREVIEW_SIZE // Get preview bytes | ||
); | ||
originalContentPreview = previewContent; | ||
originalContent = `(File too large to display inline - ${Math.round(originalContentSize / 1024)}KB. Preview shown.)`; | ||
} | ||
@@ -247,9 +263,18 @@ } | ||
try { | ||
const modifiedItemContent = await this.gitClient.getItemContent(repositoryId, filePath, projectName, change.item.objectId, undefined, true, true); | ||
// Check if the content is too large | ||
if (modifiedItemContent && modifiedItemContent.length < MAX_FILE_SIZE) { | ||
// First get the item metadata to check file size | ||
const modifiedItem = await this.gitClient.getItem(repositoryId, filePath, projectName, change.item.objectId); | ||
modifiedContentSize = modifiedItem?.contentMetadata?.contentLength || 0; | ||
// For files within the inline limit, get full content | ||
if (modifiedContentSize <= MAX_INLINE_FILE_SIZE) { | ||
const modifiedItemContent = await this.gitClient.getItemContent(repositoryId, filePath, projectName, change.item.objectId, undefined, true, true); | ||
modifiedContent = modifiedItemContent.toString('utf8'); | ||
} | ||
// For large files, get a preview | ||
else { | ||
modifiedContent = '(File too large to display)'; | ||
// Get just the beginning of the file for preview | ||
const previewContent = await this.gitClient.getItemText(repositoryId, filePath, projectName, change.item.objectId, 0, // Start at beginning | ||
PREVIEW_SIZE // Get preview bytes | ||
); | ||
modifiedContentPreview = previewContent; | ||
modifiedContent = `(File too large to display inline - ${Math.round(modifiedContentSize / 1024)}KB. Preview shown.)`; | ||
} | ||
@@ -272,2 +297,8 @@ } | ||
modifiedContent, | ||
originalContentSize, | ||
modifiedContentSize, | ||
originalContentPreview, | ||
modifiedContentPreview, | ||
isBinary, | ||
isFolder, | ||
}; | ||
@@ -282,2 +313,34 @@ return enhancedChange; | ||
/** | ||
* Get content for a specific file in a pull request by chunks | ||
* This allows retrieving parts of large files that can't be displayed inline | ||
*/ | ||
async getPullRequestFileContent(repositoryId, pullRequestId, filePath, objectId, startPosition, length, project) { | ||
await this.initialize(); | ||
if (!this.gitClient) { | ||
throw new Error('Git client not initialized'); | ||
} | ||
// Use the provided project or fall back to the default project | ||
const projectName = project || this.defaultProject; | ||
if (!projectName) { | ||
throw new Error('Project name is required'); | ||
} | ||
try { | ||
// Get metadata about the file to know its full size | ||
const item = await this.gitClient.getItem(repositoryId, filePath, projectName, objectId); | ||
const fileSize = item?.contentMetadata?.contentLength || 0; | ||
// Get the specified chunk of content | ||
const content = await this.gitClient.getItemText(repositoryId, filePath, projectName, objectId, startPosition, length); | ||
return { | ||
content, | ||
size: fileSize, | ||
position: startPosition, | ||
length: content.length, | ||
}; | ||
} | ||
catch (error) { | ||
console.error(`Error getting file content for ${filePath}:`, error); | ||
throw new Error(`Failed to retrieve content for file: ${filePath}`); | ||
} | ||
} | ||
/** | ||
* Helper function to determine if a file is likely binary based on extension | ||
@@ -284,0 +347,0 @@ */ |
@@ -216,2 +216,22 @@ #!/usr/bin/env node | ||
}); | ||
// New tool for getting content of large files in pull requests by chunks | ||
server.tool('azure_devops_pull_request_file_content', 'Get content of a specific file in a pull request by chunks (for large files)', { | ||
repositoryId: z.string().describe('Repository ID'), | ||
pullRequestId: z.number().describe('Pull request ID'), | ||
filePath: z.string().describe('File path'), | ||
objectId: z.string().describe('Object ID of the file version'), | ||
startPosition: z.number().describe('Starting position in the file (bytes)'), | ||
length: z.number().describe('Length to read (bytes)'), | ||
project: z.string().describe('Project name'), | ||
}, async ({ repositoryId, pullRequestId, filePath, objectId, startPosition, length, project }) => { | ||
const result = await azureDevOpsService.getPullRequestFileContent(repositoryId, pullRequestId, filePath, objectId, startPosition, length, project); | ||
return { | ||
content: [ | ||
{ | ||
type: 'text', | ||
text: JSON.stringify(result, null, 2), | ||
}, | ||
], | ||
}; | ||
}); | ||
// New tool for creating pull request comments | ||
@@ -276,2 +296,3 @@ server.tool('azure_devops_create_pr_comment', 'Create a comment on a pull request', { | ||
console.error('- azure_devops_pull_request_changes: Get detailed code changes for a pull request'); | ||
console.error('- azure_devops_pull_request_file_content: Get content of a specific file in chunks (for large files)'); | ||
console.error('- azure_devops_create_pr_comment: Create a comment on a pull request'); | ||
@@ -278,0 +299,0 @@ } |
@@ -161,3 +161,4 @@ #!/usr/bin/env node | ||
project: z.string().describe('Project name'), | ||
}, async ({ repositoryId, pullRequestId, project }) => { | ||
}, async ({ repositoryId, pullRequestId, project }, { signal }) => { | ||
signal.throwIfAborted(); | ||
const result = await azureDevOpsService.getPullRequestChanges(repositoryId, pullRequestId, project); | ||
@@ -173,2 +174,23 @@ return { | ||
}); | ||
// New tool for getting content of large files in pull requests by chunks | ||
server.tool('azure_devops_pull_request_file_content', 'Get content of a specific file in a pull request by chunks (for large files)', { | ||
repositoryId: z.string().describe('Repository ID'), | ||
pullRequestId: z.number().describe('Pull request ID'), | ||
filePath: z.string().describe('File path'), | ||
objectId: z.string().describe('Object ID of the file version'), | ||
startPosition: z.number().describe('Starting position in the file (bytes)'), | ||
length: z.number().describe('Length to read (bytes)'), | ||
project: z.string().describe('Project name'), | ||
}, async ({ repositoryId, pullRequestId, filePath, objectId, startPosition, length, project }, { signal }) => { | ||
signal.throwIfAborted(); | ||
const result = await azureDevOpsService.getPullRequestFileContent(repositoryId, pullRequestId, filePath, objectId, startPosition, length, project); | ||
return { | ||
content: [ | ||
{ | ||
type: 'text', | ||
text: JSON.stringify(result, null, 2), | ||
}, | ||
], | ||
}; | ||
}); | ||
// New tool for creating pull request comments | ||
@@ -175,0 +197,0 @@ server.tool('azure_devops_create_pr_comment', 'Create a comment on a pull request', { |
{ | ||
"name": "cursor-azure-devops-mcp", | ||
"version": "1.0.1", | ||
"version": "1.0.2", | ||
"description": "MCP Server for Cursor IDE-Azure DevOps Integration", | ||
@@ -5,0 +5,0 @@ "main": "build/index.js", |
194
README.md
@@ -151,4 +151,2 @@ # Cursor Azure DevOps MCP Server | ||
 | ||
#### Option 2: SSE Mode (Alternative) | ||
@@ -181,4 +179,2 @@ | ||
 | ||
### Windows Users | ||
@@ -225,2 +221,3 @@ | ||
| `azure_devops_pull_request_changes` | Get detailed PR code changes | `repositoryId` (string), `pullRequestId` (number), `project` (string) | | ||
| `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) | | ||
| `azure_devops_create_pr_comment` | Create a comment on a pull request | `repositoryId` (string), `pullRequestId` (number), `project` (string), `content` (string), and other optional parameters | | ||
@@ -311,189 +308,2 @@ | ||
npm run test-connection | ||
``` | ||
## Publishing | ||
To publish to npm: | ||
```bash | ||
npm run build | ||
npm publish | ||
``` | ||
## Continuous Integration and Deployment | ||
This project uses GitHub Actions for automated builds, testing, and publishing. | ||
### Status Badges | ||
The status badges at the top of the README provide real-time information about the project: | ||
- **CI**: Shows the status of the continuous integration workflow (build and tests) | ||
- **Publish**: Indicates the status of the npm publishing workflow | ||
- **Version Bump**: Indicates the status of the version bump workflow | ||
- **npm version**: Shows the latest published version on npm | ||
- **License**: Indicates the project license type | ||
### CI Workflow | ||
The CI workflow runs on every push to the main and develop branches, as well as on pull requests: | ||
- Builds the project with Node.js 16.x, 18.x, and 20.x | ||
- Runs linting and formatting checks | ||
- Performs security checks to prevent sensitive data exposure | ||
- Ensures the build completes successfully | ||
You can see the workflow details in `.github/workflows/ci.yml`. | ||
### CD Workflow (Publishing) | ||
The publishing workflow runs when: | ||
- A change is pushed to the main branch that modifies `package.json` or source code | ||
- A new GitHub release is created | ||
- Manually triggered via GitHub Actions interface | ||
It performs the following steps: | ||
1. Runs all checks and builds the package | ||
2. Extracts the package version from package.json | ||
3. Checks if that version already exists on npm | ||
4. If the version exists, automatically bumps the patch version | ||
5. Creates a GitHub release for the new version | ||
6. Publishes the package to npm | ||
To use this workflow, you need to: | ||
1. Add an `NPM_TOKEN` secret to your GitHub repository settings | ||
2. Either: | ||
- Push changes to the main branch (auto-versioning will handle the rest) | ||
- Manually trigger the workflow from the Actions tab | ||
- Create a GitHub release | ||
The workflow supports automatic version bumping when a version conflict is detected, making continuous delivery seamless. | ||
You can see the workflow details in `.github/workflows/publish.yml`. | ||
## Troubleshooting | ||
### Summary of Common Issues and Solutions | ||
1. **Best Practices for Connection**: | ||
- Use Command mode when possible (more reliable) | ||
- If using SSE mode, set the port directly in the command line with `PORT=9836 npm run sse-server` | ||
- Always verify the server is running before trying to connect from Cursor | ||
2. **Port Configuration**: | ||
- The most reliable way to change the port is using the environment variable directly: `PORT=9836 npm run sse-server` | ||
- Make sure to use the same port in Cursor when adding the MCP server: `http://localhost:9836/sse` | ||
3. **Session Management Issues**: | ||
- If you see "Session not found" errors in the console, try restarting both the server and Cursor | ||
- Clear Cursor's application data (Help > Clear Application Data) if problems persist | ||
4. **Connection Errors**: | ||
- For "Failed to create client" errors, make sure the server is running and accessible | ||
- Check that no firewalls or security software are blocking the connection | ||
- Try using a different port if the default port (3000) is in use | ||
### MCP Server Not Connecting | ||
- Make sure your Azure DevOps credentials in the `.env` file are correct | ||
- Check that your personal access token has the necessary permissions: | ||
- Code (read) | ||
- Pull Request Threads (read) | ||
- Work Items (read) | ||
- If using SSE mode, ensure the server is running before adding the MCP server in Cursor | ||
### Command Not Found | ||
- If running with npx, make sure you have Node.js installed | ||
- If using the global installation, try reinstalling: `npm install -g cursor-azure-devops-mcp` | ||
### SSE Connection Issues | ||
If you encounter JSON validation errors when connecting via SSE mode: | ||
1. **Use Command Mode Instead**: The command mode is more reliable and recommended for most users. | ||
2. **Check Cursor Version**: Ensure you're using Cursor version 0.46.9 or newer, as older versions had known issues with SSE connections. | ||
3. **Run with Debug Logs**: Start the SSE server with debug logs: | ||
```bash | ||
DEBUG=* npm run sse-server | ||
``` | ||
4. **Port Conflicts**: Make sure no other service is using port 3000. You can change the port by setting the `PORT` environment variable: | ||
```bash | ||
PORT=3001 npm run sse-server | ||
``` | ||
Then use `http://localhost:3001/sse` as the SSE endpoint URL. | ||
5. **Network Issues**: Make sure your firewall isn't blocking the connection. | ||
6. **Verify Server is Running**: Make sure you can access the server's home page at `http://localhost:3000` | ||
### "Cannot set headers after they are sent to the client" Error | ||
If you encounter this error when running the SSE server: | ||
1. **Check for duplicate header settings**: This error occurs when the application tries to set HTTP headers after they've already been sent. In the SSE implementation, make sure you're not manually setting headers that the `SSEServerTransport` already sets. | ||
2. **Avoid manual header manipulation**: The `SSEServerTransport` class from the MCP SDK handles the necessary headers for SSE connections. Don't set these headers manually: | ||
```javascript | ||
// Don't set these manually in your route handlers | ||
res.setHeader('Content-Type', 'text/event-stream'); | ||
res.setHeader('Cache-Control', 'no-cache, no-transform'); | ||
res.setHeader('Connection', 'keep-alive'); | ||
``` | ||
3. **Check response handling**: Ensure you're not sending multiple responses to the same request. Each HTTP request should have exactly one response. | ||
### Port Configuration Issues | ||
If you're changing the port in your `.env` file but the server still runs on port 3000, try one of these solutions: | ||
1. **Set the PORT directly in the command line**: | ||
```bash | ||
PORT=9836 npm run sse-server | ||
``` | ||
This is the most reliable way to change the port. The server will output the actual port it's using: | ||
``` | ||
Server running on port 9836 | ||
SSE endpoint: http://localhost:9836/sse | ||
Message endpoint: http://localhost:9836/message | ||
``` | ||
2. **Verify your `.env` file is being loaded**: | ||
Make sure your `.env` file is in the root directory of the project and has the correct format: | ||
``` | ||
PORT=9836 | ||
HOST=localhost | ||
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization | ||
AZURE_DEVOPS_TOKEN=your-personal-access-token | ||
AZURE_DEVOPS_PROJECT=YourProject | ||
LOG_LEVEL=info | ||
``` | ||
3. **Use Command mode instead**: | ||
The Command mode is more reliable and doesn't require managing a separate HTTP server. | ||
### Connecting to Cursor with Custom Port | ||
If you're running the SSE server on a custom port (e.g., 9836), make sure to use the correct port when adding the MCP server in Cursor: | ||
1. Start the server with your custom port: | ||
```bash | ||
PORT=9836 npm run sse-server | ||
``` | ||
2. In Cursor IDE, when adding the MCP server, use the correct port in the SSE endpoint URL: | ||
``` | ||
http://localhost:9836/sse | ||
``` | ||
## License | ||
MIT | ||
## Contributing | ||
Contributions are welcome! Please feel free to submit a Pull Request. | ||
``` |
1571
7.24%75261
-0.95%305
-38.51%