feat: add file and directory handling tools
- Added list_directory_content tool for browsing repository files/directories - Added get_file_content tool with smart truncation for large files - Smart defaults by file type (config: 200, docs: 300, code: 500 lines) - Pagination support with start_line and line_count parameters - Tail functionality using negative start_line values - Auto-truncation for files >50KB, explicit approval needed for >1MB files - Returns metadata including size, encoding, and last modified info - Added FileHandlers class following modular architecture - Added TypeScript interfaces and type guards - Updated version to 0.5.0 and CHANGELOG
This commit is contained in:
parent
b1a25d2a05
commit
c6d5b0a6f5
8 changed files with 645 additions and 2 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.5.0] - 2025-01-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **New file and directory handling tools**:
|
||||||
|
- `list_directory_content` - List files and directories in any repository path
|
||||||
|
- Shows file/directory type, size, and full paths
|
||||||
|
- Supports browsing specific branches
|
||||||
|
- Works with both Bitbucket Server and Cloud APIs
|
||||||
|
- `get_file_content` - Retrieve file content with smart truncation for large files
|
||||||
|
- Automatic smart defaults by file type (config: 200 lines, docs: 300 lines, code: 500 lines)
|
||||||
|
- Pagination support with `start_line` and `line_count` parameters
|
||||||
|
- Tail functionality using negative `start_line` values (e.g., -50 for last 50 lines)
|
||||||
|
- Automatic truncation for files >50KB to prevent token overload
|
||||||
|
- Files >1MB require explicit `full_content: true` parameter
|
||||||
|
- Returns metadata including file size, encoding, and last modified info
|
||||||
|
- Added `FileHandlers` class following existing modular architecture patterns
|
||||||
|
- Added TypeScript interfaces for file/directory entries and metadata
|
||||||
|
- Added type guards `isListDirectoryContentArgs` and `isGetFileContentArgs`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Enhanced documentation with comprehensive examples for file handling tools
|
||||||
|
|
||||||
## [0.4.0] - 2025-01-21
|
## [0.4.0] - 2025-01-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
100
README.md
100
README.md
|
@ -23,6 +23,10 @@ An MCP (Model Context Protocol) server that provides tools for interacting with
|
||||||
- `delete_branch` - Delete branches (with protection checks)
|
- `delete_branch` - Delete branches (with protection checks)
|
||||||
- `get_branch` - Get detailed branch information including associated PRs
|
- `get_branch` - Get detailed branch information including associated PRs
|
||||||
|
|
||||||
|
#### File and Directory Tools
|
||||||
|
- `list_directory_content` - List files and directories in a repository path
|
||||||
|
- `get_file_content` - Get file content with smart truncation for large files
|
||||||
|
|
||||||
#### Code Review Tools
|
#### Code Review Tools
|
||||||
- `get_pull_request_diff` - Get the diff/changes for a pull request
|
- `get_pull_request_diff` - Get the diff/changes for a pull request
|
||||||
- `approve_pull_request` - Approve a pull request
|
- `approve_pull_request` - Approve a pull request
|
||||||
|
@ -419,6 +423,102 @@ This tool is particularly useful for:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### List Directory Content
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
"tool": "list_directory_content",
|
||||||
|
"arguments": {
|
||||||
|
"workspace": "PROJ",
|
||||||
|
"repository": "my-repo",
|
||||||
|
"path": "src/components", // Optional (defaults to root)
|
||||||
|
"branch": "main" // Optional (defaults to default branch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns directory listing with:
|
||||||
|
- Path and branch information
|
||||||
|
- Array of contents with:
|
||||||
|
- Name
|
||||||
|
- Type (file or directory)
|
||||||
|
- Size (for files)
|
||||||
|
- Full path
|
||||||
|
- Total items count
|
||||||
|
|
||||||
|
### Get File Content
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
"tool": "get_file_content",
|
||||||
|
"arguments": {
|
||||||
|
"workspace": "PROJ",
|
||||||
|
"repository": "my-repo",
|
||||||
|
"file_path": "src/index.ts",
|
||||||
|
"branch": "main", // Optional (defaults to default branch)
|
||||||
|
"start_line": 1, // Optional: starting line (1-based, use negative for from end)
|
||||||
|
"line_count": 100, // Optional: number of lines to return
|
||||||
|
"full_content": false // Optional: force full content (default: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Smart Truncation Features:**
|
||||||
|
- Automatically truncates large files (>50KB) to prevent token overload
|
||||||
|
- Default line counts based on file type:
|
||||||
|
- Config files (.yml, .json): 200 lines
|
||||||
|
- Documentation (.md, .txt): 300 lines
|
||||||
|
- Code files (.ts, .js, .py): 500 lines
|
||||||
|
- Log files: Last 100 lines
|
||||||
|
- Use `start_line: -50` to get last 50 lines (tail functionality)
|
||||||
|
- Files larger than 1MB require explicit `full_content: true` or line parameters
|
||||||
|
|
||||||
|
Returns file content with:
|
||||||
|
- File path and branch
|
||||||
|
- File size and encoding
|
||||||
|
- Content (full or truncated based on parameters)
|
||||||
|
- Line information (if truncated):
|
||||||
|
- Total lines in file
|
||||||
|
- Range of returned lines
|
||||||
|
- Truncation indicator
|
||||||
|
- Last modified information (commit, author, date)
|
||||||
|
|
||||||
|
Example responses:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Small file - returns full content
|
||||||
|
{
|
||||||
|
"file_path": "package.json",
|
||||||
|
"branch": "main",
|
||||||
|
"size": 1234,
|
||||||
|
"encoding": "utf-8",
|
||||||
|
"content": "{\n \"name\": \"my-project\",\n ...",
|
||||||
|
"last_modified": {
|
||||||
|
"commit_id": "abc123",
|
||||||
|
"author": "John Doe",
|
||||||
|
"date": "2025-01-21T10:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large file - automatically truncated
|
||||||
|
{
|
||||||
|
"file_path": "src/components/LargeComponent.tsx",
|
||||||
|
"branch": "main",
|
||||||
|
"size": 125000,
|
||||||
|
"encoding": "utf-8",
|
||||||
|
"content": "... first 500 lines ...",
|
||||||
|
"line_info": {
|
||||||
|
"total_lines": 3500,
|
||||||
|
"returned_lines": {
|
||||||
|
"start": 1,
|
||||||
|
"end": 500
|
||||||
|
},
|
||||||
|
"truncated": true,
|
||||||
|
"message": "Showing lines 1-500 of 3500. File size: 122.1KB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
- `npm run dev` - Watch mode for development
|
- `npm run dev` - Watch mode for development
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nexus2520/bitbucket-mcp-server",
|
"name": "@nexus2520/bitbucket-mcp-server",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "MCP server for Bitbucket API integration - supports both Cloud and Server",
|
"description": "MCP server for Bitbucket API integration - supports both Cloud and Server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|
352
src/handlers/file-handlers.ts
Normal file
352
src/handlers/file-handlers.ts
Normal file
|
@ -0,0 +1,352 @@
|
||||||
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { BitbucketApiClient } from '../utils/api-client.js';
|
||||||
|
import {
|
||||||
|
isListDirectoryContentArgs,
|
||||||
|
isGetFileContentArgs
|
||||||
|
} from '../types/guards.js';
|
||||||
|
import {
|
||||||
|
BitbucketServerDirectoryEntry,
|
||||||
|
BitbucketCloudDirectoryEntry,
|
||||||
|
BitbucketCloudFileMetadata
|
||||||
|
} from '../types/bitbucket.js';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export class FileHandlers {
|
||||||
|
// Default lines by file extension
|
||||||
|
private readonly DEFAULT_LINES_BY_EXT: Record<string, number> = {
|
||||||
|
'.yml': 200, '.yaml': 200, '.json': 200, // Config files
|
||||||
|
'.md': 300, '.txt': 300, // Docs
|
||||||
|
'.ts': 500, '.js': 500, '.py': 500, // Code
|
||||||
|
'.tsx': 500, '.jsx': 500, '.java': 500, // More code
|
||||||
|
'.log': -100 // Last 100 lines for logs
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private apiClient: BitbucketApiClient,
|
||||||
|
private baseUrl: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handleListDirectoryContent(args: any) {
|
||||||
|
if (!isListDirectoryContentArgs(args)) {
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.InvalidParams,
|
||||||
|
'Invalid arguments for list_directory_content'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workspace, repository, path: dirPath = '', branch } = args;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let apiPath: string;
|
||||||
|
let params: any = {};
|
||||||
|
let response: any;
|
||||||
|
|
||||||
|
if (this.apiClient.getIsServer()) {
|
||||||
|
// Bitbucket Server API
|
||||||
|
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/browse`;
|
||||||
|
if (dirPath) {
|
||||||
|
apiPath += `/${dirPath}`;
|
||||||
|
}
|
||||||
|
if (branch) {
|
||||||
|
params.at = `refs/heads/${branch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
|
||||||
|
} else {
|
||||||
|
// Bitbucket Cloud API
|
||||||
|
const branchOrDefault = branch || 'HEAD';
|
||||||
|
apiPath = `/repositories/${workspace}/${repository}/src/${branchOrDefault}`;
|
||||||
|
if (dirPath) {
|
||||||
|
apiPath += `/${dirPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await this.apiClient.makeRequest<any>('get', apiPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the response
|
||||||
|
let contents: any[] = [];
|
||||||
|
let actualBranch = branch;
|
||||||
|
|
||||||
|
if (this.apiClient.getIsServer()) {
|
||||||
|
// Bitbucket Server response
|
||||||
|
const entries = response.children?.values || [];
|
||||||
|
contents = entries.map((entry: BitbucketServerDirectoryEntry) => ({
|
||||||
|
name: entry.path.name,
|
||||||
|
type: entry.type === 'FILE' ? 'file' : 'directory',
|
||||||
|
size: entry.size,
|
||||||
|
path: dirPath ? `${dirPath}/${entry.path.name}` : entry.path.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get the actual branch from the response if available
|
||||||
|
if (!branch && response.path?.components) {
|
||||||
|
// Server returns default branch info in the response
|
||||||
|
actualBranch = 'default';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bitbucket Cloud response
|
||||||
|
const entries = response.values || [];
|
||||||
|
contents = entries.map((entry: BitbucketCloudDirectoryEntry) => ({
|
||||||
|
name: entry.path.split('/').pop() || entry.path,
|
||||||
|
type: entry.type === 'commit_file' ? 'file' : 'directory',
|
||||||
|
size: entry.size,
|
||||||
|
path: entry.path
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Cloud returns the branch in the response
|
||||||
|
actualBranch = branch || response.commit?.branch || 'main';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
path: dirPath || '/',
|
||||||
|
branch: actualBranch,
|
||||||
|
contents,
|
||||||
|
total_items: contents.length
|
||||||
|
}, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return this.apiClient.handleApiError(error, `listing directory '${dirPath}' in ${workspace}/${repository}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleGetFileContent(args: any) {
|
||||||
|
if (!isGetFileContentArgs(args)) {
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.InvalidParams,
|
||||||
|
'Invalid arguments for get_file_content'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workspace, repository, file_path, branch, start_line, line_count, full_content = false } = args;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let fileContent: string;
|
||||||
|
let fileMetadata: any = {};
|
||||||
|
const fileSizeLimit = 1024 * 1024; // 1MB default limit
|
||||||
|
|
||||||
|
if (this.apiClient.getIsServer()) {
|
||||||
|
// Bitbucket Server - get file metadata first to check size
|
||||||
|
const browsePath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/browse/${file_path}`;
|
||||||
|
const browseParams: any = {};
|
||||||
|
if (branch) {
|
||||||
|
browseParams.at = `refs/heads/${branch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const metadataResponse = await this.apiClient.makeRequest<any>('get', browsePath, undefined, { params: browseParams });
|
||||||
|
fileMetadata = {
|
||||||
|
size: metadataResponse.size || 0,
|
||||||
|
path: file_path
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check file size
|
||||||
|
if (!full_content && fileMetadata.size > fileSizeLimit) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
error: 'File too large',
|
||||||
|
file_path,
|
||||||
|
size: fileMetadata.size,
|
||||||
|
size_mb: (fileMetadata.size / (1024 * 1024)).toFixed(2),
|
||||||
|
message: `File exceeds size limit. Use full_content: true to force retrieval or use start_line/line_count for partial content.`
|
||||||
|
}, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If browse fails, continue to try raw endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get raw content
|
||||||
|
const rawPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/raw/${file_path}`;
|
||||||
|
const rawParams: any = {};
|
||||||
|
if (branch) {
|
||||||
|
rawParams.at = `refs/heads/${branch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.apiClient.makeRequest<any>('get', rawPath, undefined, {
|
||||||
|
params: rawParams,
|
||||||
|
responseType: 'text',
|
||||||
|
headers: { 'Accept': 'text/plain' }
|
||||||
|
});
|
||||||
|
|
||||||
|
fileContent = response;
|
||||||
|
} else {
|
||||||
|
// Bitbucket Cloud - first get metadata
|
||||||
|
const branchOrDefault = branch || 'HEAD';
|
||||||
|
const metaPath = `/repositories/${workspace}/${repository}/src/${branchOrDefault}/${file_path}`;
|
||||||
|
|
||||||
|
const metadataResponse = await this.apiClient.makeRequest<BitbucketCloudFileMetadata>('get', metaPath);
|
||||||
|
|
||||||
|
fileMetadata = {
|
||||||
|
size: metadataResponse.size,
|
||||||
|
encoding: metadataResponse.encoding,
|
||||||
|
path: metadataResponse.path,
|
||||||
|
commit: metadataResponse.commit
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check file size
|
||||||
|
if (!full_content && fileMetadata.size > fileSizeLimit) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
error: 'File too large',
|
||||||
|
file_path,
|
||||||
|
size: fileMetadata.size,
|
||||||
|
size_mb: (fileMetadata.size / (1024 * 1024)).toFixed(2),
|
||||||
|
message: `File exceeds size limit. Use full_content: true to force retrieval or use start_line/line_count for partial content.`
|
||||||
|
}, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow the download link to get actual content
|
||||||
|
const downloadUrl = metadataResponse.links.download.href;
|
||||||
|
const downloadResponse = await this.apiClient.makeRequest<any>('get', downloadUrl, undefined, {
|
||||||
|
baseURL: '', // Use full URL
|
||||||
|
responseType: 'text',
|
||||||
|
headers: { 'Accept': 'text/plain' }
|
||||||
|
});
|
||||||
|
|
||||||
|
fileContent = downloadResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply line filtering if requested
|
||||||
|
let processedContent = fileContent;
|
||||||
|
let lineInfo: any = null;
|
||||||
|
|
||||||
|
if (!full_content || start_line !== undefined || line_count !== undefined) {
|
||||||
|
const lines = fileContent.split('\n');
|
||||||
|
const totalLines = lines.length;
|
||||||
|
|
||||||
|
// Determine default line count based on file extension
|
||||||
|
const ext = path.extname(file_path).toLowerCase();
|
||||||
|
const defaultLineCount = this.DEFAULT_LINES_BY_EXT[ext] || 500;
|
||||||
|
const shouldUseTail = defaultLineCount < 0;
|
||||||
|
|
||||||
|
// Calculate start and end indices
|
||||||
|
let startIdx: number;
|
||||||
|
let endIdx: number;
|
||||||
|
|
||||||
|
if (start_line !== undefined) {
|
||||||
|
if (start_line < 0) {
|
||||||
|
// Negative start_line means from end
|
||||||
|
startIdx = Math.max(0, totalLines + start_line);
|
||||||
|
endIdx = totalLines;
|
||||||
|
} else {
|
||||||
|
// 1-based to 0-based index
|
||||||
|
startIdx = Math.max(0, start_line - 1);
|
||||||
|
endIdx = startIdx + (line_count || Math.abs(defaultLineCount));
|
||||||
|
}
|
||||||
|
} else if (!full_content && fileMetadata.size > 50 * 1024) {
|
||||||
|
// Auto-truncate large files
|
||||||
|
if (shouldUseTail) {
|
||||||
|
startIdx = Math.max(0, totalLines + defaultLineCount);
|
||||||
|
endIdx = totalLines;
|
||||||
|
} else {
|
||||||
|
startIdx = 0;
|
||||||
|
endIdx = Math.abs(defaultLineCount);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Return full content for small files
|
||||||
|
startIdx = 0;
|
||||||
|
endIdx = totalLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure indices are within bounds
|
||||||
|
startIdx = Math.max(0, Math.min(startIdx, totalLines));
|
||||||
|
endIdx = Math.max(startIdx, Math.min(endIdx, totalLines));
|
||||||
|
|
||||||
|
// Extract the requested lines
|
||||||
|
const selectedLines = lines.slice(startIdx, endIdx);
|
||||||
|
processedContent = selectedLines.join('\n');
|
||||||
|
|
||||||
|
lineInfo = {
|
||||||
|
total_lines: totalLines,
|
||||||
|
returned_lines: {
|
||||||
|
start: startIdx + 1,
|
||||||
|
end: endIdx
|
||||||
|
},
|
||||||
|
truncated: startIdx > 0 || endIdx < totalLines,
|
||||||
|
message: endIdx < totalLines
|
||||||
|
? `Showing lines ${startIdx + 1}-${endIdx} of ${totalLines}. File size: ${(fileMetadata.size / 1024).toFixed(1)}KB`
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
const response: any = {
|
||||||
|
file_path,
|
||||||
|
branch: branch || (this.apiClient.getIsServer() ? 'default' : 'main'),
|
||||||
|
size: fileMetadata.size || fileContent.length,
|
||||||
|
encoding: fileMetadata.encoding || 'utf-8',
|
||||||
|
content: processedContent
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lineInfo) {
|
||||||
|
response.line_info = lineInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileMetadata.commit) {
|
||||||
|
response.last_modified = {
|
||||||
|
commit_id: fileMetadata.commit.hash,
|
||||||
|
author: fileMetadata.commit.author?.user?.display_name || fileMetadata.commit.author?.raw,
|
||||||
|
date: fileMetadata.commit.date,
|
||||||
|
message: fileMetadata.commit.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(response, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle specific not found error
|
||||||
|
if (error.status === 404) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `File '${file_path}' not found in ${workspace}/${repository}${branch ? ` on branch '${branch}'` : ''}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.apiClient.handleApiError(error, `getting file content for '${file_path}' in ${workspace}/${repository}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get default line count based on file extension
|
||||||
|
private getDefaultLines(filePath: string, fileSize: number): { full: boolean } | { start: number; count: number } {
|
||||||
|
// Small files: return full content
|
||||||
|
if (fileSize < 50 * 1024) { // 50KB
|
||||||
|
return { full: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
const defaultLines = this.DEFAULT_LINES_BY_EXT[ext] || 500;
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: defaultLines < 0 ? defaultLines : 1,
|
||||||
|
count: Math.abs(defaultLines)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
11
src/index.ts
11
src/index.ts
|
@ -12,6 +12,7 @@ import { BitbucketApiClient } from './utils/api-client.js';
|
||||||
import { PullRequestHandlers } from './handlers/pull-request-handlers.js';
|
import { PullRequestHandlers } from './handlers/pull-request-handlers.js';
|
||||||
import { BranchHandlers } from './handlers/branch-handlers.js';
|
import { BranchHandlers } from './handlers/branch-handlers.js';
|
||||||
import { ReviewHandlers } from './handlers/review-handlers.js';
|
import { ReviewHandlers } from './handlers/review-handlers.js';
|
||||||
|
import { FileHandlers } from './handlers/file-handlers.js';
|
||||||
import { toolDefinitions } from './tools/definitions.js';
|
import { toolDefinitions } from './tools/definitions.js';
|
||||||
|
|
||||||
// Get environment variables
|
// Get environment variables
|
||||||
|
@ -33,12 +34,13 @@ class BitbucketMCPServer {
|
||||||
private pullRequestHandlers: PullRequestHandlers;
|
private pullRequestHandlers: PullRequestHandlers;
|
||||||
private branchHandlers: BranchHandlers;
|
private branchHandlers: BranchHandlers;
|
||||||
private reviewHandlers: ReviewHandlers;
|
private reviewHandlers: ReviewHandlers;
|
||||||
|
private fileHandlers: FileHandlers;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.server = new Server(
|
this.server = new Server(
|
||||||
{
|
{
|
||||||
name: 'bitbucket-mcp-server',
|
name: 'bitbucket-mcp-server',
|
||||||
version: '0.4.0',
|
version: '0.5.0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
|
@ -63,6 +65,7 @@ class BitbucketMCPServer {
|
||||||
);
|
);
|
||||||
this.branchHandlers = new BranchHandlers(this.apiClient, BITBUCKET_BASE_URL);
|
this.branchHandlers = new BranchHandlers(this.apiClient, BITBUCKET_BASE_URL);
|
||||||
this.reviewHandlers = new ReviewHandlers(this.apiClient, BITBUCKET_USERNAME!);
|
this.reviewHandlers = new ReviewHandlers(this.apiClient, BITBUCKET_USERNAME!);
|
||||||
|
this.fileHandlers = new FileHandlers(this.apiClient, BITBUCKET_BASE_URL);
|
||||||
|
|
||||||
this.setupToolHandlers();
|
this.setupToolHandlers();
|
||||||
|
|
||||||
|
@ -117,6 +120,12 @@ class BitbucketMCPServer {
|
||||||
case 'remove_requested_changes':
|
case 'remove_requested_changes':
|
||||||
return this.reviewHandlers.handleRemoveRequestedChanges(request.params.arguments);
|
return this.reviewHandlers.handleRemoveRequestedChanges(request.params.arguments);
|
||||||
|
|
||||||
|
// File tools
|
||||||
|
case 'list_directory_content':
|
||||||
|
return this.fileHandlers.handleListDirectoryContent(request.params.arguments);
|
||||||
|
case 'get_file_content':
|
||||||
|
return this.fileHandlers.handleGetFileContent(request.params.arguments);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new McpError(
|
throw new McpError(
|
||||||
ErrorCode.MethodNotFound,
|
ErrorCode.MethodNotFound,
|
||||||
|
|
|
@ -416,4 +416,68 @@ export const toolDefinitions = [
|
||||||
required: ['workspace', 'repository', 'branch_name'],
|
required: ['workspace', 'repository', 'branch_name'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'list_directory_content',
|
||||||
|
description: 'List files and directories in a repository path',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
workspace: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Repository slug (e.g., "my-repo")',
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Directory path (optional, defaults to root, e.g., "src/components")',
|
||||||
|
},
|
||||||
|
branch: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Branch name (optional, defaults to default branch)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['workspace', 'repository'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_file_content',
|
||||||
|
description: 'Get file content from a repository with smart truncation for large files',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
workspace: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Repository slug (e.g., "my-repo")',
|
||||||
|
},
|
||||||
|
file_path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Path to the file (e.g., "src/index.ts")',
|
||||||
|
},
|
||||||
|
branch: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Branch name (optional, defaults to default branch)',
|
||||||
|
},
|
||||||
|
start_line: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Starting line number (1-based). Use negative for lines from end (optional)',
|
||||||
|
},
|
||||||
|
line_count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of lines to return (optional, default varies by file size)',
|
||||||
|
},
|
||||||
|
full_content: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Force return full content regardless of size (optional, default: false)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['workspace', 'repository', 'file_path'],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -115,6 +115,17 @@ export interface BitbucketServerBranch {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bitbucket Server Directory Entry
|
||||||
|
export interface BitbucketServerDirectoryEntry {
|
||||||
|
path: {
|
||||||
|
name: string;
|
||||||
|
toString: string;
|
||||||
|
};
|
||||||
|
type: 'FILE' | 'DIRECTORY';
|
||||||
|
size?: number;
|
||||||
|
contentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Bitbucket Cloud API response types
|
// Bitbucket Cloud API response types
|
||||||
export interface BitbucketCloudPullRequest {
|
export interface BitbucketCloudPullRequest {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -195,6 +206,55 @@ export interface BitbucketCloudBranch {
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bitbucket Cloud Directory Entry
|
||||||
|
export interface BitbucketCloudDirectoryEntry {
|
||||||
|
path: string;
|
||||||
|
type: 'commit_file' | 'commit_directory';
|
||||||
|
size?: number;
|
||||||
|
commit?: {
|
||||||
|
hash: string;
|
||||||
|
};
|
||||||
|
links?: {
|
||||||
|
self: {
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
html: {
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitbucket Cloud File Metadata
|
||||||
|
export interface BitbucketCloudFileMetadata {
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
encoding?: string;
|
||||||
|
mimetype?: string;
|
||||||
|
links: {
|
||||||
|
self: {
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
html: {
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
download: {
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
commit?: {
|
||||||
|
hash: string;
|
||||||
|
author?: {
|
||||||
|
raw: string;
|
||||||
|
user?: {
|
||||||
|
display_name: string;
|
||||||
|
account_id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
date?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Merge info type for enhanced PR details
|
// Merge info type for enhanced PR details
|
||||||
export interface MergeInfo {
|
export interface MergeInfo {
|
||||||
mergeCommitHash?: string;
|
mergeCommitHash?: string;
|
||||||
|
|
|
@ -202,3 +202,39 @@ export const isGetBranchArgs = (
|
||||||
typeof args.repository === 'string' &&
|
typeof args.repository === 'string' &&
|
||||||
typeof args.branch_name === 'string' &&
|
typeof args.branch_name === 'string' &&
|
||||||
(args.include_merged_prs === undefined || typeof args.include_merged_prs === 'boolean');
|
(args.include_merged_prs === undefined || typeof args.include_merged_prs === 'boolean');
|
||||||
|
|
||||||
|
export const isListDirectoryContentArgs = (
|
||||||
|
args: any
|
||||||
|
): args is {
|
||||||
|
workspace: string;
|
||||||
|
repository: string;
|
||||||
|
path?: string;
|
||||||
|
branch?: string;
|
||||||
|
} =>
|
||||||
|
typeof args === 'object' &&
|
||||||
|
args !== null &&
|
||||||
|
typeof args.workspace === 'string' &&
|
||||||
|
typeof args.repository === 'string' &&
|
||||||
|
(args.path === undefined || typeof args.path === 'string') &&
|
||||||
|
(args.branch === undefined || typeof args.branch === 'string');
|
||||||
|
|
||||||
|
export const isGetFileContentArgs = (
|
||||||
|
args: any
|
||||||
|
): args is {
|
||||||
|
workspace: string;
|
||||||
|
repository: string;
|
||||||
|
file_path: string;
|
||||||
|
branch?: string;
|
||||||
|
start_line?: number;
|
||||||
|
line_count?: number;
|
||||||
|
full_content?: boolean;
|
||||||
|
} =>
|
||||||
|
typeof args === 'object' &&
|
||||||
|
args !== null &&
|
||||||
|
typeof args.workspace === 'string' &&
|
||||||
|
typeof args.repository === 'string' &&
|
||||||
|
typeof args.file_path === 'string' &&
|
||||||
|
(args.branch === undefined || typeof args.branch === 'string') &&
|
||||||
|
(args.start_line === undefined || typeof args.start_line === 'number') &&
|
||||||
|
(args.line_count === undefined || typeof args.line_count === 'number') &&
|
||||||
|
(args.full_content === undefined || typeof args.full_content === 'boolean');
|
||||||
|
|
Loading…
Reference in a new issue