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/),
|
||||
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
|
||||
|
||||
### 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)
|
||||
- `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
|
||||
- `get_pull_request_diff` - Get the diff/changes for 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
|
||||
|
||||
- `npm run dev` - Watch mode for development
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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",
|
||||
"type": "module",
|
||||
"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 { BranchHandlers } from './handlers/branch-handlers.js';
|
||||
import { ReviewHandlers } from './handlers/review-handlers.js';
|
||||
import { FileHandlers } from './handlers/file-handlers.js';
|
||||
import { toolDefinitions } from './tools/definitions.js';
|
||||
|
||||
// Get environment variables
|
||||
|
@ -33,12 +34,13 @@ class BitbucketMCPServer {
|
|||
private pullRequestHandlers: PullRequestHandlers;
|
||||
private branchHandlers: BranchHandlers;
|
||||
private reviewHandlers: ReviewHandlers;
|
||||
private fileHandlers: FileHandlers;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'bitbucket-mcp-server',
|
||||
version: '0.4.0',
|
||||
version: '0.5.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
|
@ -63,6 +65,7 @@ class BitbucketMCPServer {
|
|||
);
|
||||
this.branchHandlers = new BranchHandlers(this.apiClient, BITBUCKET_BASE_URL);
|
||||
this.reviewHandlers = new ReviewHandlers(this.apiClient, BITBUCKET_USERNAME!);
|
||||
this.fileHandlers = new FileHandlers(this.apiClient, BITBUCKET_BASE_URL);
|
||||
|
||||
this.setupToolHandlers();
|
||||
|
||||
|
@ -117,6 +120,12 @@ class BitbucketMCPServer {
|
|||
case 'remove_requested_changes':
|
||||
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:
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
|
|
|
@ -416,4 +416,68 @@ export const toolDefinitions = [
|
|||
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
|
||||
export interface BitbucketCloudPullRequest {
|
||||
id: number;
|
||||
|
@ -195,6 +206,55 @@ export interface BitbucketCloudBranch {
|
|||
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
|
||||
export interface MergeInfo {
|
||||
mergeCommitHash?: string;
|
||||
|
|
|
@ -202,3 +202,39 @@ export const isGetBranchArgs = (
|
|||
typeof args.repository === 'string' &&
|
||||
typeof args.branch_name === 'string' &&
|
||||
(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