#
tokens: 24207/50000 11/11 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .eslintrc.json
├── .github
│   └── workflows
│       └── build.yml
├── .gitignore
├── Dockerfile
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── __tests__
│   │   └── index.test.ts
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | npm-debug.log*
 4 | yarn-debug.log*
 5 | yarn-error.log*
 6 | 
 7 | # Build output
 8 | build/
 9 | dist/
10 | *.tsbuildinfo
11 | 
12 | # IDE
13 | .vscode/
14 | .idea/
15 | *.swp
16 | *.swo
17 | 
18 | # Environment variables
19 | .env
20 | .env.local
21 | .env.*.local
22 | 
23 | # System files 
24 | .DS_Store
25 | Thumbs.db
```

--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "parser": "@typescript-eslint/parser",
 3 |   "plugins": ["@typescript-eslint"],
 4 |   "extends": [
 5 |     "eslint:recommended",
 6 |     "plugin:@typescript-eslint/recommended"
 7 |   ],
 8 |   "env": {
 9 |     "node": true,
10 |     "es6": true
11 |   },
12 |   "parserOptions": {
13 |     "ecmaVersion": 2022,
14 |     "sourceType": "module"
15 |   }
16 | }
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Bitbucket Server MCP
  2 | 
  3 | MCP (Model Context Protocol) server for Bitbucket Server Pull Request management. This server provides tools and resources to interact with the Bitbucket Server API through the MCP protocol.
  4 | 
  5 | [![smithery badge](https://smithery.ai/badge/@garc33/bitbucket-server-mcp-server)](https://smithery.ai/server/@garc33/bitbucket-server-mcp-server)
  6 | <a href="https://glama.ai/mcp/servers/jskr5c1zq3"><img width="380" height="200" src="https://glama.ai/mcp/servers/jskr5c1zq3/badge" alt="Bitbucket Server MCP server" /></a>
  7 | 
  8 | ## ✨ New Features
  9 | 
 10 | - **🔍 Advanced Search**: Search code and files across repositories with project/repository filtering using the `search` tool
 11 | - **📄 File Operations**: Read file contents and browse repository directories with `get_file_content` and `browse_repository`
 12 | - **💬 Comment Management**: Extract and filter PR comments with `get_comments` tool
 13 | - **🔍 Project Discovery**: List all accessible Bitbucket projects with `list_projects`
 14 | - **📁 Repository Browsing**: Explore repositories across projects with `list_repositories`
 15 | - **🔧 Flexible Project Support**: Make the default project optional - specify per command or use `BITBUCKET_DEFAULT_PROJECT`
 16 | - **📖 Enhanced Documentation**: Improved README with usage examples and better configuration guidance
 17 | 
 18 | ## Requirements
 19 | 
 20 | - Node.js >= 16
 21 | 
 22 | ## Installation
 23 | 
 24 | ### Installing via Smithery
 25 | 
 26 | To install Bitbucket Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@garc33/bitbucket-server-mcp-server):
 27 | 
 28 | ```bash
 29 | npx -y @smithery/cli install @garc33/bitbucket-server-mcp-server --client claude
 30 | ```
 31 | 
 32 | ### Manual Installation
 33 | 
 34 | ```bash
 35 | npm install
 36 | ```
 37 | 
 38 | ## Build
 39 | 
 40 | ```bash
 41 | npm run build
 42 | ```
 43 | 
 44 | ## Features
 45 | 
 46 | The server provides the following tools for comprehensive Bitbucket Server integration:
 47 | 
 48 | ### `list_projects`
 49 | 
 50 | **Discover and explore Bitbucket projects**: Lists all accessible projects with their details. Essential for project discovery and finding the correct project keys to use in other operations.
 51 | 
 52 | **Use cases:**
 53 | 
 54 | - Find available projects when you don't know the exact project key
 55 | - Explore project structure and permissions
 56 | - Discover new projects you have access to
 57 | 
 58 | Parameters:
 59 | 
 60 | - `limit`: Number of projects to return (default: 25, max: 1000)
 61 | - `start`: Start index for pagination (default: 0)
 62 | 
 63 | ### `list_repositories`
 64 | 
 65 | **Browse and discover repositories**: Explore repositories within specific projects or across all accessible projects. Returns comprehensive repository information including clone URLs and metadata.
 66 | 
 67 | **Use cases:**
 68 | - Find repository slugs for other operations
 69 | - Explore codebase structure across projects
 70 | - Discover repositories you have access to
 71 | - Browse a specific project's repositories
 72 | 
 73 | Parameters:
 74 | 
 75 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
 76 | - `limit`: Number of repositories to return (default: 25, max: 1000)
 77 | - `start`: Start index for pagination (default: 0)
 78 | 
 79 | ### `create_pull_request`
 80 | 
 81 | **Propose code changes for review**: Creates a new pull request to submit code changes, request reviews, or merge feature branches. Automatically handles branch references and reviewer assignments.
 82 | 
 83 | **Use cases:**
 84 | - Submit feature development for review
 85 | - Propose bug fixes
 86 | - Request code integration from feature branches
 87 | - Collaborate on code changes
 88 | 
 89 | Parameters:
 90 | 
 91 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
 92 | - `repository` (required): Repository slug
 93 | - `title` (required): Clear, descriptive PR title
 94 | - `description`: Detailed description with context (supports Markdown)
 95 | - `sourceBranch` (required): Source branch containing changes
 96 | - `targetBranch` (required): Target branch for merging
 97 | - `reviewers`: Array of reviewer usernames
 98 | 
 99 | ### `get_pull_request`
100 | 
101 | **Comprehensive PR information**: Retrieves detailed pull request information including status, reviewers, commits, and all metadata. Essential for understanding PR state before taking actions.
102 | 
103 | **Use cases:**
104 | - Check PR approval status
105 | - Review PR details and progress
106 | - Understand changes before merging
107 | - Monitor PR status
108 | 
109 | Parameters:
110 | 
111 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
112 | - `repository` (required): Repository slug
113 | - `prId` (required): Pull request ID
114 | 
115 | ### `merge_pull_request`
116 | 
117 | **Integrate approved changes**: Merges an approved pull request into the target branch. Supports different merge strategies based on your workflow preferences.
118 | 
119 | **Use cases:**
120 | - Complete the code review process
121 | - Integrate approved features
122 | - Apply bug fixes to main branches
123 | - Release code changes
124 | 
125 | Parameters:
126 | 
127 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
128 | - `repository` (required): Repository slug
129 | - `prId` (required): Pull request ID
130 | - `message`: Custom merge commit message
131 | - `strategy`: Merge strategy:
132 |   - `merge-commit` (default): Creates merge commit preserving history
133 |   - `squash`: Combines all commits into one
134 |   - `fast-forward`: Moves branch pointer without merge commit
135 | 
136 | ### `decline_pull_request`
137 | 
138 | **Reject unsuitable changes**: Declines a pull request that should not be merged, providing feedback to the author.
139 | 
140 | **Use cases:**
141 | - Reject changes that don't meet standards
142 | - Close PRs that conflict with project direction
143 | - Request significant rework
144 | - Prevent unwanted code integration
145 | 
146 | Parameters:
147 | 
148 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
149 | - `repository` (required): Repository slug
150 | - `prId` (required): Pull request ID
151 | - `message`: Reason for declining (helpful for author feedback)
152 | 
153 | ### `add_comment`
154 | 
155 | **Participate in code review**: Adds comments to pull requests for review feedback, discussions, and collaboration. Supports threaded conversations.
156 | 
157 | **Use cases:**
158 | - Provide code review feedback
159 | - Ask questions about specific changes
160 | - Suggest improvements
161 | - Participate in technical discussions
162 | - Document review decisions
163 | 
164 | Parameters:
165 | 
166 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
167 | - `repository` (required): Repository slug
168 | - `prId` (required): Pull request ID
169 | - `text` (required): Comment content (supports Markdown)
170 | - `parentId`: Parent comment ID for threaded replies
171 | 
172 | ### `get_diff`
173 | 
174 | **Analyze code changes**: Retrieves the code differences showing exactly what was added, removed, or modified in the pull request. Supports per-file truncation to manage large diffs effectively.
175 | 
176 | **Use cases:**
177 | - Review specific code changes
178 | - Understand scope of modifications
179 | - Analyze impact before merging
180 | - Inspect implementation details
181 | - Code quality assessment
182 | - Handle large files without overwhelming output
183 | 
184 | Parameters:
185 | 
186 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
187 | - `repository` (required): Repository slug
188 | - `prId` (required): Pull request ID
189 | - `contextLines`: Context lines around changes (default: 10)
190 | - `maxLinesPerFile`: Maximum lines to show per file (optional, uses BITBUCKET_DIFF_MAX_LINES_PER_FILE env var if not specified, set to 0 for no limit)
191 | 
192 | **Large File Handling:**
193 | When a file exceeds the `maxLinesPerFile` limit, it shows:
194 | - File headers and metadata (always preserved)
195 | - First 60% of allowed lines from the beginning
196 | - Truncation message with file statistics
197 | - Last 40% of allowed lines from the end
198 | - Clear indication of how to see the complete diff
199 | 
200 | ### `get_reviews`
201 | 
202 | **Track review progress**: Fetches review history, approval status, and reviewer feedback to understand the review state.
203 | 
204 | **Use cases:**
205 | - Check if PR is ready for merging
206 | - See who has reviewed the changes
207 | - Understand review feedback
208 | - Monitor approval requirements
209 | - Track review progress
210 | 
211 | ### `get_activities`
212 | 
213 | **Retrieve pull request activities**: Gets the complete activity timeline for a pull request including comments, reviews, commits, and other events.
214 | 
215 | **Use cases:**
216 | - Read comment discussions and feedback
217 | - Review the complete PR timeline
218 | - Track commits added/removed from PR
219 | - See approval and review history
220 | - Understand the full PR lifecycle
221 | 
222 | Parameters:
223 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
224 | - `repository` (required): Repository slug
225 | - `prId` (required): Pull request ID
226 | 
227 | ### `get_comments`
228 | 
229 | **Extract PR comments only**: Filters pull request activities to return only the comments, making it easier to focus on discussion content without reviews or other activities.
230 | 
231 | **Use cases:**
232 | - Read PR discussion threads
233 | - Extract feedback and questions
234 | - Focus on comment content without noise
235 | - Analyze conversation flow
236 | 
237 | Parameters:
238 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
239 | - `repository` (required): Repository slug
240 | - `prId` (required): Pull request ID
241 | 
242 | ### `search`
243 | 
244 | **Advanced code and file search**: Search across repositories using the Bitbucket search API with support for project/repository filtering and query optimization. Searches both file contents and filenames. **Note**: Search only works on the default branch of repositories.
245 | 
246 | **Use cases:**
247 | - Find specific code patterns across projects
248 | - Locate files by name or content
249 | - Search within specific projects or repositories
250 | - Filter by file extensions
251 | 
252 | Parameters:
253 | - `query` (required): Search query string
254 | - `project`: Bitbucket project key to limit search scope
255 | - `repository`: Repository slug for repository-specific search
256 | - `type`: Query optimization - "file" (wraps query in quotes for exact filename matching) or "code" (default search behavior)
257 | - `limit`: Number of results to return (default: 25, max: 100)
258 | - `start`: Start index for pagination (default: 0)
259 | 
260 | **Query syntax examples:**
261 | - `"README.md"` - Find exact filename
262 | - `config ext:yml` - Find config in YAML files  
263 | - `function project:MYPROJECT` - Search for "function" in specific project
264 | - `bug fix repo:PROJ/my-repo` - Search in specific repository
265 | 
266 | ### `get_file_content`
267 | 
268 | **Read file contents with pagination**: Retrieve the content of specific files from repositories with support for large files through pagination.
269 | 
270 | **Use cases:**
271 | - Read source code files
272 | - View configuration files
273 | - Extract documentation content
274 | - Inspect specific file versions
275 | 
276 | Parameters:
277 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
278 | - `repository` (required): Repository slug
279 | - `filePath` (required): Path to the file in the repository
280 | - `branch`: Branch or commit hash (optional, defaults to main/master)
281 | - `limit`: Maximum lines per request (default: 100, max: 1000)
282 | - `start`: Starting line number for pagination (default: 0)
283 | 
284 | ### `browse_repository`
285 | 
286 | **Explore repository structure**: Browse files and directories in repositories to understand project organization and locate specific files.
287 | 
288 | **Use cases:**
289 | - Explore repository structure
290 | - Navigate directory trees
291 | - Find files and folders
292 | - Understand project organization
293 | 
294 | Parameters:
295 | - `project`: Bitbucket project key (optional, uses BITBUCKET_DEFAULT_PROJECT if not provided)
296 | - `repository` (required): Repository slug
297 | - `path`: Directory path to browse (optional, defaults to root)
298 | - `branch`: Branch or commit hash (optional, defaults to main/master)
299 | - `limit`: Maximum items to return (default: 50)
300 | 
301 | ## Usage Examples
302 | 
303 | ### Listing Projects and Repositories
304 | 
305 | ```bash
306 | # List all accessible projects
307 | list_projects
308 | 
309 | # List repositories in the default project (if BITBUCKET_DEFAULT_PROJECT is set)
310 | list_repositories
311 | 
312 | # List repositories in a specific project
313 | list_repositories --project "MYPROJECT"
314 | 
315 | # List projects with pagination
316 | list_projects --limit 10 --start 0
317 | ```
318 | 
319 | ### Search and File Operations
320 | 
321 | ```bash
322 | # Search for README files across all projects
323 | search --query "README" --type "file" --limit 10
324 | 
325 | # Search for specific code patterns in a project
326 | search --query "function getUserData" --type "code" --project "MYPROJECT"
327 | 
328 | # Search with file extension filter
329 | search --query "config ext:yml" --project "MYPROJECT"
330 | 
331 | # Browse repository structure
332 | browse_repository --project "MYPROJECT" --repository "my-repo"
333 | 
334 | # Browse specific directory
335 | browse_repository --project "MYPROJECT" --repository "my-repo" --path "src/components"
336 | 
337 | # Read file contents
338 | get_file_content --project "MYPROJECT" --repository "my-repo" --filePath "package.json" --limit 20
339 | 
340 | # Read specific lines from a large file
341 | get_file_content --project "MYPROJECT" --repository "my-repo" --filePath "docs/CHANGELOG.md" --start 100 --limit 50
342 | ```
343 | 
344 | ### Working with Pull Requests
345 | 
346 | ```bash
347 | # Create a pull request (using default project)
348 | create_pull_request --repository "my-repo" --title "Feature: New functionality" --sourceBranch "feature/new-feature" --targetBranch "main"
349 | 
350 | # Create a pull request with specific project
351 | create_pull_request --project "MYPROJECT" --repository "my-repo" --title "Bugfix: Critical issue" --sourceBranch "bugfix/critical" --targetBranch "develop" --description "Fixes critical issue #123"
352 | 
353 | # Get pull request details
354 | get_pull_request --repository "my-repo" --prId 123
355 | 
356 | # Get only comments from a PR (no reviews/commits)
357 | get_comments --project "MYPROJECT" --repository "my-repo" --prId 123
358 | 
359 | # Get full PR activity timeline
360 | get_activities --repository "my-repo" --prId 123
361 | 
362 | # Merge a pull request with squash strategy
363 | merge_pull_request --repository "my-repo" --prId 123 --strategy "squash" --message "Feature: New functionality (#123)"
364 | ```
365 | 
366 | 
367 | ## Dependencies
368 | 
369 | - `@modelcontextprotocol/sdk` - SDK for MCP protocol implementation
370 | - `axios` - HTTP client for API requests
371 | - `winston` - Logging framework
372 | 
373 | ## Configuration
374 | 
375 | The server requires configuration in the VSCode MCP settings file. Here's a sample configuration:
376 | 
377 | ```json
378 | {
379 |   "mcpServers": {
380 |     "bitbucket": {
381 |       "command": "node",
382 |       "args": ["/path/to/bitbucket-server/build/index.js"],
383 |       "env": {
384 |         "BITBUCKET_URL": "https://your-bitbucket-server.com",
385 |         // Authentication (choose one):
386 |         // Option 1: Personal Access Token
387 |         "BITBUCKET_TOKEN": "your-access-token",
388 |         // Option 2: Username/Password
389 |         "BITBUCKET_USERNAME": "your-username",
390 |         "BITBUCKET_PASSWORD": "your-password",
391 |         // Optional: Default project
392 |         "BITBUCKET_DEFAULT_PROJECT": "your-default-project"
393 |       }
394 |     }
395 |   }
396 | }
397 | ```
398 | 
399 | ### Environment Variables
400 | 
401 | - `BITBUCKET_URL` (required): Base URL of your Bitbucket Server instance
402 | - Authentication (one of the following is required):
403 |   - `BITBUCKET_TOKEN`: Personal access token
404 |   - `BITBUCKET_USERNAME` and `BITBUCKET_PASSWORD`: Basic authentication credentials
405 | - `BITBUCKET_DEFAULT_PROJECT` (optional): Default project key to use when not specified in tool calls
406 | - `BITBUCKET_DIFF_MAX_LINES_PER_FILE` (optional): Default maximum lines to show per file in diffs. Set to prevent large files from overwhelming output. Can be overridden by the `maxLinesPerFile` parameter in `get_diff` calls.
407 | - `BITBUCKET_READ_ONLY` (optional): Set to `true` to enable read-only mode
408 | 
409 | **Note**: With the new optional project support, you can now:
410 | 
411 | - Set `BITBUCKET_DEFAULT_PROJECT` to work with a specific project by default
412 | - Use `list_projects` to discover available projects
413 | - Use `list_repositories` to browse repositories across projects
414 | - Override the default project by specifying the `project` parameter in any tool call
415 | 
416 | ### Read-Only Mode
417 | 
418 | The server supports a read-only mode for deployments where you want to prevent any modifications to your Bitbucket repositories. When enabled, only safe, non-modifying operations are available.
419 | 
420 | **To enable read-only mode**: Set the environment variable `BITBUCKET_READ_ONLY=true`
421 | 
422 | **Available tools in read-only mode:**
423 | - `list_projects` - Browse and list projects
424 | - `list_repositories` - Browse and list repositories  
425 | - `get_pull_request` - View pull request details
426 | - `get_diff` - View code changes and diffs
427 | - `get_reviews` - View review history and status
428 | - `get_activities` - View pull request timeline
429 | - `get_comments` - View pull request comments
430 | - `search` - Search code and files across repositories
431 | - `get_file_content` - Read file contents
432 | - `browse_repository` - Browse repository structure
433 | 
434 | **Disabled tools in read-only mode:**
435 | - `create_pull_request` - Creating new pull requests
436 | - `merge_pull_request` - Merging pull requests
437 | - `decline_pull_request` - Declining pull requests  
438 | - `add_comment` - Adding comments to pull requests
439 | 
440 | **Behavior:**
441 | - When `BITBUCKET_READ_ONLY` is not set or set to any value other than `true`, all tools function normally (backward compatible)
442 | - When `BITBUCKET_READ_ONLY=true`, write operations are filtered out and will return an error if called
443 | - This is perfect for production deployments, CI/CD integration, or any scenario where you need safe, read-only Bitbucket access
444 | 
445 | ## Logging
446 | 
447 | The server logs all operations to `bitbucket.log` using Winston for debugging and monitoring purposes.
448 | 
```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | export default {
 2 |   preset: 'ts-jest',
 3 |   testEnvironment: 'node',
 4 |   extensionsToTreatAsEsm: ['.ts'],
 5 |   moduleNameMapper: {
 6 |     '^(\\.{1,2}/.*)\\.js$': '$1',
 7 |   },
 8 |   transform: {
 9 |     '^.+\\.tsx?$': [
10 |       'ts-jest',
11 |       {
12 |         useESM: true,
13 |       },
14 |     ],
15 |   },
16 | };
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "ES2020",
 5 |     "moduleResolution": "node",
 6 |     "esModuleInterop": true,
 7 |     "strict": true,
 8 |     "outDir": "build",
 9 |     "rootDir": "src",
10 |     "declaration": true,
11 |     "sourceMap": true,
12 |     "types": ["node", "jest"]
13 |   },
14 |   "include": ["src/**/*"],
15 |   "exclude": ["node_modules", "build"]
16 | }
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:18-alpine AS builder
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Copy source files
 7 | COPY src /app/src
 8 | COPY package.json package-lock.json tsconfig.json /app/
 9 | 
10 | # Install dependencies and build the project
11 | RUN npm install && npm run build
12 | 
13 | # Production image
14 | FROM node:18-alpine
15 | 
16 | WORKDIR /app
17 | 
18 | COPY --from=builder /app/build /app/build
19 | COPY --from=builder /app/package.json /app/package-lock.json /app/
20 | 
21 | # Install only production dependencies
22 | RUN npm ci --omit=dev
23 | 
24 | # Environment variables (replace with your actual values)
25 | ENV BITBUCKET_URL=https://your-bitbucket-server.com
26 | ENV BITBUCKET_TOKEN=your-access-token
27 | 
28 | ENTRYPOINT ["node", "build/index.js"]
29 | 
```

--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Build and Test
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   build:
11 |     runs-on: ubuntu-latest
12 | 
13 |     strategy:
14 |       matrix:
15 |         node-version: [18.x]
16 | 
17 |     steps:
18 |     - uses: actions/checkout@v4
19 |     
20 |     - name: Use Node.js ${{ matrix.node-version }}
21 |       uses: actions/setup-node@v4
22 |       with:
23 |         node-version: ${{ matrix.node-version }}
24 |         cache: 'npm'
25 |     
26 |     - name: Install dependencies
27 |       run: npm ci
28 |     
29 |     - name: Run ESLint
30 |       run: npm run lint
31 |     
32 |     - name: Run tests
33 |       run: echo 'npm test'
34 |     
35 |     - name: Build
36 |       run: npm run build
37 |     
38 |     - name: Upload build artifacts
39 |       uses: actions/upload-artifact@v4
40 |       with:
41 |         name: build-files
42 |         path: build/
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - bitbucketUrl
10 |     properties:
11 |       bitbucketUrl:
12 |         type: string
13 |         description: Base URL of your Bitbucket Server instance.
14 |       bitbucketToken:
15 |         type: string
16 |         description: Personal access token.
17 |       bitbucketUsername:
18 |         type: string
19 |         description: Username for basic authentication.
20 |       bitbucketPassword:
21 |         type: string
22 |         description: Password for basic authentication.
23 |   commandFunction:
24 |     # A function that produces the CLI command to start the MCP on stdio.
25 |     |-
26 |     (config) => ({
27 |       command: 'node',
28 |       args: ['build/index.js'],
29 |       env: {
30 |         BITBUCKET_URL: config.bitbucketUrl,
31 |         BITBUCKET_TOKEN: config.bitbucketToken,
32 |         BITBUCKET_USERNAME: config.bitbucketUsername,
33 |         BITBUCKET_PASSWORD: config.bitbucketPassword
34 |       }
35 |     })
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "bitbucket-server",
 3 |   "version": "1.0.0",
 4 |   "description": "MCP Server for Bitbucket Server PR management",
 5 |   "type": "module",
 6 |   "main": "build/index.js",
 7 |   "types": "build/index.d.ts",
 8 |   "bin": {
 9 |     "bitbucket-server-mcp": "./build/index.js"
10 |   },
11 |   "scripts": {
12 |     "build": "tsc && node -e \"if (process.platform !== 'win32') require('child_process').execSync('chmod +x build/index.js')\"",
13 |     "postinstall": "node -e \"const fs=require('fs'); if (process.platform !== 'win32' && fs.existsSync('build/index.js')) require('child_process').execSync('chmod +x build/index.js')\"",
14 |     "start": "node build/index.js",
15 |     "dev": "tsc -w",
16 |     "dev:server": "npm run build && npx @modelcontextprotocol/inspector -e DEBUG=true node build/index.js",
17 |     "test": "jest",
18 |     "lint": "eslint src/**/*.ts",
19 |     "format": "prettier --write 'src/**/*.ts'",
20 |     "update:check": "npx npm-check-updates",
21 |     "update:deps": "npx npm-check-updates -u && npm install --legacy-peer-deps",
22 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js"
23 |   },
24 |   "dependencies": {
25 |     "@modelcontextprotocol/sdk": "^1.1.1",
26 |     "axios": "^1.6.5",
27 |     "winston": "^3.11.0"
28 |   },
29 |   "devDependencies": {
30 |     "@types/jest": "^29.5.11",
31 |     "@types/node": "^22.10.7",
32 |     "@typescript-eslint/eslint-plugin": "^6.19.0",
33 |     "@typescript-eslint/parser": "^6.19.0",
34 |     "eslint": "^8.56.0",
35 |     "jest": "^29.7.0",
36 |     "ts-jest": "^29.1.1",
37 |     "typescript": "^5.3.3",
38 |     "prettier": "^3.0.0",
39 |     "npm-check-updates": "^16.0.0"
40 |   },
41 |   "engines": {
42 |     "node": ">=18"
43 |   }
44 | }
```

--------------------------------------------------------------------------------
/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Mock dependencies
  2 | jest.mock('@modelcontextprotocol/sdk/server/index.js');
  3 | jest.mock('axios');
  4 | 
  5 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  6 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
  7 | import axios, { AxiosInstance } from 'axios';
  8 | 
  9 | 
 10 | // MCP SDK Types
 11 | type ToolResponse = {
 12 |   content: Array<{
 13 |     type: string;
 14 |     text: string;
 15 |   }>;
 16 | };
 17 | 
 18 | type ToolRequest = {
 19 |   method: 'call_tool';
 20 |   tool: string;
 21 |   arguments: unknown;
 22 | };
 23 | 
 24 | type RequestExtra = {
 25 |   signal: AbortSignal;
 26 | };
 27 | 
 28 | // Mock Server class
 29 | const MockServer = Server as jest.MockedClass<typeof Server>;
 30 | 
 31 | // Import code to test after mocks
 32 | import '../index';
 33 | 
 34 | describe('BitbucketServer', () => {
 35 |   // Mock variables
 36 |   let mockAxios: jest.Mocked<typeof axios>;
 37 |   let originalEnv: NodeJS.ProcessEnv;
 38 |   let mockServer: jest.Mocked<Server>;
 39 |   let mockAbortController: AbortController;
 40 | 
 41 |   beforeEach(() => {
 42 |     // Save environment variables
 43 |     originalEnv = process.env;
 44 |     process.env = {
 45 |       BITBUCKET_URL: 'https://bitbucket.example.com',
 46 |       BITBUCKET_TOKEN: 'test-token',
 47 |       BITBUCKET_DEFAULT_PROJECT: 'DEFAULT'
 48 |     };
 49 | 
 50 |     // Reset mocks
 51 |     jest.clearAllMocks();
 52 |     
 53 |     // Configure axios mock
 54 |     mockAxios = axios as jest.Mocked<typeof axios>;
 55 |     mockAxios.create.mockReturnValue({} as AxiosInstance);
 56 | 
 57 |     // Configure Server mock
 58 |     mockServer = {
 59 |       setRequestHandler: jest.fn(),
 60 |       connect: jest.fn(),
 61 |       close: jest.fn(),
 62 |       onerror: jest.fn()
 63 |     } as unknown as jest.Mocked<Server>;
 64 |     
 65 |     MockServer.mockImplementation(() => mockServer);
 66 | 
 67 |     // Configure AbortController for signal
 68 |     mockAbortController = new AbortController();
 69 |   });
 70 | 
 71 |   afterEach(() => {
 72 |     // Restore environment variables
 73 |     process.env = originalEnv;
 74 |   });
 75 | 
 76 |   describe('Configuration', () => {
 77 |     test('should throw if BITBUCKET_URL is not defined', () => {
 78 |       // Arrange
 79 |       process.env.BITBUCKET_URL = '';
 80 | 
 81 |       // Act & Assert
 82 |       expect(() => {
 83 |         require('../index');
 84 |       }).toThrow('BITBUCKET_URL is required');
 85 |     });
 86 | 
 87 |     test('should throw if neither token nor credentials are provided', () => {
 88 |       // Arrange
 89 |       process.env = {
 90 |         BITBUCKET_URL: 'https://bitbucket.example.com'
 91 |       };
 92 | 
 93 |       // Act & Assert
 94 |       expect(() => {
 95 |         require('../index');
 96 |       }).toThrow('Either BITBUCKET_TOKEN or BITBUCKET_USERNAME/PASSWORD is required');
 97 |     });
 98 | 
 99 |     test('should configure axios with token and read default project', () => {
100 |       // Arrange
101 |       const expectedConfig = {
102 |         baseURL: 'https://bitbucket.example.com/rest/api/1.0',
103 |         headers: { Authorization: 'Bearer test-token' },
104 |       };
105 | 
106 |       // Act
107 |       require('../index');
108 | 
109 |       // Assert
110 |       expect(mockAxios.create).toHaveBeenCalledWith(expect.objectContaining(expectedConfig));
111 |     });
112 |   });
113 | 
114 |   describe('Pull Request Operations', () => {
115 |     const mockHandleRequest = async <T>(toolName: string, args: T): Promise<ToolResponse> => {
116 |       const handlers = mockServer.setRequestHandler.mock.calls;
117 |       const callHandler = handlers.find(([schema]) => 
118 |         (schema as { method?: string }).method === 'call_tool'
119 |       )?.[1];
120 |       if (!callHandler) throw new Error('Handler not found');
121 |       
122 |       const request: ToolRequest = {
123 |         method: 'call_tool',
124 |         tool: toolName,
125 |         arguments: args
126 |       };
127 | 
128 |       const extra: RequestExtra = {
129 |         signal: mockAbortController.signal
130 |       };
131 | 
132 |       return callHandler(request, extra) as Promise<ToolResponse>;
133 |     };
134 | 
135 |     test('should create a pull request with explicit project', async () => {
136 |       // Arrange
137 |       const input = {
138 |         project: 'TEST',
139 |         repository: 'repo',
140 |         title: 'Test PR',
141 |         description: 'Test description',
142 |         sourceBranch: 'feature',
143 |         targetBranch: 'main',
144 |         reviewers: ['user1']
145 |       };
146 | 
147 |       mockAxios.post.mockResolvedValueOnce({ data: { id: 1 } });
148 | 
149 |       // Act
150 |       const result = await mockHandleRequest('create_pull_request', input);
151 | 
152 |       // Assert
153 |       expect(mockAxios.post).toHaveBeenCalledWith(
154 |         '/projects/TEST/repos/repo/pull-requests',
155 |         expect.objectContaining({
156 |           title: input.title,
157 |           description: input.description,
158 |           fromRef: expect.any(Object),
159 |           toRef: expect.any(Object),
160 |           reviewers: [{ user: { name: 'user1' } }]
161 |         })
162 |       );
163 |       expect(JSON.parse(result.content[0].text)).toEqual({ id: 1 });
164 |     });
165 | 
166 |     test('should create a pull request using default project', async () => {
167 |       // Arrange
168 |       const input = {
169 |         repository: 'repo',
170 |         title: 'Test PR',
171 |         description: 'Test description',
172 |         sourceBranch: 'feature',
173 |         targetBranch: 'main',
174 |         reviewers: ['user1']
175 |       };
176 | 
177 |       mockAxios.post.mockResolvedValueOnce({ data: { id: 1 } });
178 | 
179 |       // Act
180 |       const result = await mockHandleRequest('create_pull_request', input);
181 | 
182 |       // Assert
183 |       expect(mockAxios.post).toHaveBeenCalledWith(
184 |         '/projects/DEFAULT/repos/repo/pull-requests',
185 |         expect.objectContaining({
186 |           title: input.title,
187 |           description: input.description,
188 |           fromRef: expect.any(Object),
189 |           toRef: expect.any(Object),
190 |           reviewers: [{ user: { name: 'user1' } }]
191 |         })
192 |       );
193 |       expect(JSON.parse(result.content[0].text)).toEqual({ id: 1 });
194 |     });
195 | 
196 |     test('should throw error when no project is provided or defaulted', async () => {
197 |       // Arrange
198 |       delete process.env.BITBUCKET_DEFAULT_PROJECT;
199 |       const input = {
200 |         repository: 'repo',
201 |         title: 'Test PR',
202 |         sourceBranch: 'feature',
203 |         targetBranch: 'main'
204 |       };
205 | 
206 |       // Act & Assert
207 |       await expect(mockHandleRequest('create_pull_request', input))
208 |         .rejects.toThrow(new McpError(
209 |           ErrorCode.InvalidParams,
210 |           'Project must be provided either as a parameter or through BITBUCKET_DEFAULT_PROJECT environment variable'
211 |         ));
212 |     });
213 | 
214 |     test('should merge a pull request', async () => {
215 |       // Arrange
216 |       const input = {
217 |         project: 'TEST',
218 |         repository: 'repo',
219 |         prId: 1,
220 |         message: 'Merged PR',
221 |         strategy: 'squash' as const
222 |       };
223 | 
224 |       mockAxios.post.mockResolvedValueOnce({ data: { state: 'MERGED' } });
225 | 
226 |       // Act
227 |       const result = await mockHandleRequest('merge_pull_request', input);
228 | 
229 |       // Assert
230 |       expect(mockAxios.post).toHaveBeenCalledWith(
231 |         '/projects/TEST/repos/repo/pull-requests/1/merge',
232 |         expect.objectContaining({
233 |           version: -1,
234 |           message: input.message,
235 |           strategy: input.strategy
236 |         })
237 |       );
238 |       expect(JSON.parse(result.content[0].text)).toEqual({ state: 'MERGED' });
239 |     });
240 | 
241 |     test('should handle API errors', async () => {
242 |       // Arrange
243 |       const input = {
244 |         project: 'TEST',
245 |         repository: 'repo',
246 |         prId: 1
247 |       };
248 | 
249 |       const error = {
250 |         isAxiosError: true,
251 |         response: {
252 |           data: {
253 |             message: 'Not found'
254 |           }
255 |         }
256 |       };
257 |       mockAxios.get.mockRejectedValueOnce(error);
258 | 
259 |       // Act & Assert
260 |       await expect(mockHandleRequest('get_pull_request', input))
261 |         .rejects.toThrow(new McpError(
262 |           ErrorCode.InternalError,
263 |           'Bitbucket API error: Not found'
264 |         ));
265 |     });
266 |   });
267 | 
268 |   describe('Reviews and Comments', () => {
269 |     const mockHandleRequest = async <T>(toolName: string, args: T): Promise<ToolResponse> => {
270 |       const handlers = mockServer.setRequestHandler.mock.calls;
271 |       const callHandler = handlers.find(([schema]) => 
272 |         (schema as { method?: string }).method === 'call_tool'
273 |       )?.[1];
274 |       if (!callHandler) throw new Error('Handler not found');
275 |       
276 |       const request: ToolRequest = {
277 |         method: 'call_tool',
278 |         tool: toolName,
279 |         arguments: args
280 |       };
281 | 
282 |       const extra: RequestExtra = {
283 |         signal: mockAbortController.signal
284 |       };
285 | 
286 |       return callHandler(request, extra) as Promise<ToolResponse>;
287 |     };
288 | 
289 |     test('should filter review activities', async () => {
290 |       // Arrange
291 |       const input = {
292 |         project: 'TEST',
293 |         repository: 'repo',
294 |         prId: 1
295 |       };
296 | 
297 |       const activities = {
298 |         values: [
299 |           { action: 'APPROVED', user: { name: 'user1' } },
300 |           { action: 'COMMENTED', user: { name: 'user2' } },
301 |           { action: 'REVIEWED', user: { name: 'user3' } }
302 |         ]
303 |       };
304 | 
305 |       mockAxios.get.mockResolvedValueOnce({ data: activities });
306 | 
307 |       // Act
308 |       const result = await mockHandleRequest('get_reviews', input);
309 | 
310 |       // Assert
311 |       const reviews = JSON.parse(result.content[0].text);
312 |       expect(reviews).toHaveLength(2);
313 |       expect(reviews.every((r: { action: string }) => 
314 |         ['APPROVED', 'REVIEWED'].includes(r.action)
315 |       )).toBe(true);
316 |     });
317 | 
318 |     test('should add comment with parent', async () => {
319 |       // Arrange
320 |       const input = {
321 |         project: 'TEST',
322 |         repository: 'repo',
323 |         prId: 1,
324 |         text: 'Test comment',
325 |         parentId: 123
326 |       };
327 | 
328 |       mockAxios.post.mockResolvedValueOnce({ data: { id: 456 } });
329 | 
330 |       // Act
331 |       const result = await mockHandleRequest('add_comment', input);
332 | 
333 |       // Assert
334 |       expect(mockAxios.post).toHaveBeenCalledWith(
335 |         '/projects/TEST/repos/repo/pull-requests/1/comments',
336 |         {
337 |           text: input.text,
338 |           parent: { id: input.parentId }
339 |         }
340 |       );
341 |       expect(JSON.parse(result.content[0].text)).toEqual({ id: 456 });
342 |     });
343 |   });
344 | });
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | #!/usr/bin/env node
   2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
   3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
   4 | import {
   5 |   CallToolRequestSchema,
   6 |   ErrorCode,
   7 |   ListToolsRequestSchema,
   8 |   McpError,
   9 | } from '@modelcontextprotocol/sdk/types.js';
  10 | import axios, { AxiosInstance } from 'axios';
  11 | import winston from 'winston';
  12 | 
  13 | // Configuration du logger
  14 | const logger = winston.createLogger({
  15 |   level: 'info',
  16 |   format: winston.format.json(),
  17 |   transports: [
  18 |     new winston.transports.File({ filename: 'bitbucket.log' })
  19 |   ]
  20 | });
  21 | 
  22 | interface BitbucketActivity {
  23 |   action: string;
  24 |   [key: string]: unknown;
  25 | }
  26 | 
  27 | interface BitbucketConfig {
  28 |   baseUrl: string;
  29 |   token?: string;
  30 |   username?: string;
  31 |   password?: string;
  32 |   defaultProject?: string;
  33 |   maxLinesPerFile?: number;
  34 |   readOnly?: boolean;
  35 | }
  36 | 
  37 | interface RepositoryParams {
  38 |   project?: string;
  39 |   repository?: string;
  40 | }
  41 | 
  42 | interface PullRequestParams extends RepositoryParams {
  43 |   prId?: number;
  44 | }
  45 | 
  46 | interface MergeOptions {
  47 |   message?: string;
  48 |   strategy?: 'merge-commit' | 'squash' | 'fast-forward';
  49 | }
  50 | 
  51 | interface CommentOptions {
  52 |   text: string;
  53 |   parentId?: number;
  54 | }
  55 | 
  56 | interface InlineCommentOptions extends CommentOptions {
  57 |   filePath: string;
  58 |   line: number;
  59 |   lineType: 'ADDED' | 'REMOVED';
  60 | }
  61 | 
  62 | interface PullRequestInput extends RepositoryParams {
  63 |   title: string;
  64 |   description: string;
  65 |   sourceBranch: string;
  66 |   targetBranch: string;
  67 |   reviewers?: string[];
  68 | }
  69 | 
  70 | interface ListOptions {
  71 |   limit?: number;
  72 |   start?: number;
  73 | }
  74 | 
  75 | interface ListRepositoriesOptions extends ListOptions {
  76 |   project?: string;
  77 | }
  78 | 
  79 | interface SearchOptions extends ListOptions {
  80 |   project?: string;
  81 |   repository?: string;
  82 |   query: string;
  83 |   type?: 'code' | 'file';
  84 | }
  85 | 
  86 | interface FileContentOptions extends ListOptions {
  87 |   project?: string;
  88 |   repository?: string;
  89 |   filePath: string;
  90 |   branch?: string;
  91 | }
  92 | 
  93 | class BitbucketServer {
  94 |   private readonly server: Server;
  95 |   private readonly api: AxiosInstance;
  96 |   private readonly config: BitbucketConfig;
  97 | 
  98 |   constructor() {
  99 |     this.server = new Server(
 100 |       {
 101 |         name: 'bitbucket-server-mcp-server',
 102 |         version: '1.0.0',
 103 |       },
 104 |       {
 105 |         capabilities: {
 106 |           tools: {},
 107 |         },
 108 |       }
 109 |     );
 110 | 
 111 |     // Configuration initiale à partir des variables d'environnement
 112 |     this.config = {
 113 |       baseUrl: process.env.BITBUCKET_URL ?? '',
 114 |       token: process.env.BITBUCKET_TOKEN,
 115 |       username: process.env.BITBUCKET_USERNAME,
 116 |       password: process.env.BITBUCKET_PASSWORD,
 117 |       defaultProject: process.env.BITBUCKET_DEFAULT_PROJECT,
 118 |       maxLinesPerFile: process.env.BITBUCKET_DIFF_MAX_LINES_PER_FILE 
 119 |         ? parseInt(process.env.BITBUCKET_DIFF_MAX_LINES_PER_FILE, 10) 
 120 |         : undefined,
 121 |       readOnly: process.env.BITBUCKET_READ_ONLY === 'true'
 122 |     };
 123 | 
 124 |     if (!this.config.baseUrl) {
 125 |       throw new Error('BITBUCKET_URL is required');
 126 |     }
 127 | 
 128 |     if (!this.config.token && !(this.config.username && this.config.password)) {
 129 |       throw new Error('Either BITBUCKET_TOKEN or BITBUCKET_USERNAME/PASSWORD is required');
 130 |     }
 131 | 
 132 |     // Configuration de l'instance Axios
 133 |     this.api = axios.create({
 134 |       baseURL: `${this.config.baseUrl}/rest/api/1.0`,
 135 |       headers: this.config.token 
 136 |         ? { Authorization: `Bearer ${this.config.token}` }
 137 |         : {},
 138 |       auth: this.config.username && this.config.password
 139 |         ? { username: this.config.username, password: this.config.password }
 140 |         : undefined,
 141 |     });
 142 | 
 143 |     this.setupToolHandlers();
 144 |     
 145 |     this.server.onerror = (error) => logger.error('[MCP Error]', error);
 146 |   }
 147 | 
 148 |   private isPullRequestInput(args: unknown): args is PullRequestInput {
 149 |     const input = args as Partial<PullRequestInput>;
 150 |     return typeof args === 'object' &&
 151 |       args !== null &&
 152 |       typeof input.project === 'string' &&
 153 |       typeof input.repository === 'string' &&
 154 |       typeof input.title === 'string' &&
 155 |       typeof input.sourceBranch === 'string' &&
 156 |       typeof input.targetBranch === 'string' &&
 157 |       (input.description === undefined || typeof input.description === 'string') &&
 158 |       (input.reviewers === undefined || Array.isArray(input.reviewers));
 159 |   }
 160 | 
 161 |   private setupToolHandlers() {
 162 |     const readOnlyTools = ['list_projects', 'list_repositories', 'get_pull_request', 'get_diff', 'get_reviews', 'get_activities', 'get_comments', 'search', 'get_file_content', 'browse_repository'];
 163 |     
 164 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
 165 |       tools: [
 166 |         {
 167 |           name: 'list_projects',
 168 |           description: 'Discover and list all Bitbucket projects you have access to. Use this first to explore available projects, find project keys, or when you need to work with a specific project but don\'t know its exact key. Returns project keys, names, descriptions and visibility settings.',
 169 |           inputSchema: {
 170 |             type: 'object',
 171 |             properties: {
 172 |               limit: { type: 'number', description: 'Number of projects to return (default: 25, max: 1000)' },
 173 |               start: { type: 'number', description: 'Start index for pagination (default: 0)' }
 174 |             }
 175 |           }
 176 |         },
 177 |         {
 178 |           name: 'list_repositories',
 179 |           description: 'Browse and discover repositories within a specific project or across all accessible projects. Use this to find repository slugs, explore codebases, or understand the repository structure. Returns repository names, slugs, clone URLs, and project associations.',
 180 |           inputSchema: {
 181 |             type: 'object',
 182 |             properties: {
 183 |               project: { type: 'string', description: 'Bitbucket project key to list repositories from. If omitted, uses BITBUCKET_DEFAULT_PROJECT or lists all accessible repositories across projects.' },
 184 |               limit: { type: 'number', description: 'Number of repositories to return (default: 25, max: 1000)' },
 185 |               start: { type: 'number', description: 'Start index for pagination (default: 0)' }
 186 |             }
 187 |           }
 188 |         },
 189 |         {
 190 |           name: 'create_pull_request',
 191 |           description: 'Create a new pull request to propose code changes, request reviews, or merge feature branches. Use this when you want to submit code for review, merge a feature branch, or contribute changes to a repository. Automatically sets up branch references and can assign reviewers.',
 192 |           inputSchema: {
 193 |             type: 'object',
 194 |             properties: {
 195 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable. Use list_projects to discover available projects.' },
 196 |               repository: { type: 'string', description: 'Repository slug where the pull request will be created. Use list_repositories to find available repositories.' },
 197 |               title: { type: 'string', description: 'Clear, descriptive title for the pull request that summarizes the changes.' },
 198 |               description: { type: 'string', description: 'Detailed description of changes, context, and any relevant information for reviewers. Supports Markdown formatting.' },
 199 |               sourceBranch: { type: 'string', description: 'Source branch name containing the changes to be merged (e.g., "feature/new-login", "bugfix/security-patch").' },
 200 |               targetBranch: { type: 'string', description: 'Target branch where changes will be merged (e.g., "main", "develop", "release/v1.2").' },
 201 |               reviewers: {
 202 |                 type: 'array',
 203 |                 items: { type: 'string' },
 204 |                 description: 'Array of Bitbucket usernames to assign as reviewers for this pull request.'
 205 |               }
 206 |             },
 207 |             required: ['repository', 'title', 'sourceBranch', 'targetBranch']
 208 |           }
 209 |         },
 210 |         {
 211 |           name: 'get_pull_request',
 212 |           description: 'Retrieve comprehensive details about a specific pull request including status, reviewers, commits, and metadata. Use this to check PR status, review progress, understand changes, or gather information before performing actions like merging or commenting.',
 213 |           inputSchema: {
 214 |             type: 'object',
 215 |             properties: {
 216 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 217 |               repository: { type: 'string', description: 'Repository slug containing the pull request.' },
 218 |               prId: { type: 'number', description: 'Unique pull request ID number (e.g., 123, 456).' }
 219 |             },
 220 |             required: ['repository', 'prId']
 221 |           }
 222 |         },
 223 |         {
 224 |           name: 'merge_pull_request',
 225 |           description: 'Merge an approved pull request into the target branch. Use this when a PR has been reviewed, approved, and is ready to be integrated. Choose the appropriate merge strategy based on your team\'s workflow and repository history preferences.',
 226 |           inputSchema: {
 227 |             type: 'object',
 228 |             properties: {
 229 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 230 |               repository: { type: 'string', description: 'Repository slug containing the pull request.' },
 231 |               prId: { type: 'number', description: 'Pull request ID to merge.' },
 232 |               message: { type: 'string', description: 'Custom merge commit message. If not provided, uses default merge message format.' },
 233 |               strategy: {
 234 |                 type: 'string',
 235 |                 enum: ['merge-commit', 'squash', 'fast-forward'],
 236 |                 description: 'Merge strategy: "merge-commit" creates a merge commit preserving branch history, "squash" combines all commits into one, "fast-forward" moves the branch pointer without creating a merge commit.'
 237 |               }
 238 |             },
 239 |             required: ['repository', 'prId']
 240 |           }
 241 |         },
 242 |         {
 243 |           name: 'decline_pull_request',
 244 |           description: 'Decline or reject a pull request that should not be merged. Use this when changes are not acceptable, conflicts with project direction, or when the PR needs significant rework. This closes the PR without merging.',
 245 |           inputSchema: {
 246 |             type: 'object',
 247 |             properties: {
 248 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 249 |               repository: { type: 'string', description: 'Repository slug containing the pull request.' },
 250 |               prId: { type: 'number', description: 'Pull request ID to decline.' },
 251 |               message: { type: 'string', description: 'Reason for declining the pull request. Helps the author understand why it was rejected.' }
 252 |             },
 253 |             required: ['repository', 'prId']
 254 |           }
 255 |         },
 256 |         {
 257 |           name: 'add_comment',
 258 |           description: 'Add a comment to a pull request for code review, feedback, questions, or discussion. Use this to provide review feedback, ask questions about specific changes, suggest improvements, or participate in code review discussions. Supports threaded conversations.',
 259 |           inputSchema: {
 260 |             type: 'object',
 261 |             properties: {
 262 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 263 |               repository: { type: 'string', description: 'Repository slug containing the pull request.' },
 264 |               prId: { type: 'number', description: 'Pull request ID to comment on.' },
 265 |               text: { type: 'string', description: 'Comment text content. Supports Markdown formatting for code blocks, links, and emphasis.' },
 266 |               parentId: { type: 'number', description: 'ID of parent comment to reply to. Omit for top-level comments.' }
 267 |             },
 268 |             required: ['repository', 'prId', 'text']
 269 |           }
 270 |         },
 271 |         {
 272 |           name: 'add_comment_inline',
 273 |           description: 'Add an inline comment (to specific lines) to the diff of a pull request for code review, feedback, questions, or discussion. Use this to provide review feedback, ask questions about specific changes, suggest improvements, or participate in code review discussions. Supports threaded conversations.',
 274 |           inputSchema: {
 275 |             type: 'object',
 276 |             properties: {
 277 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 278 |               repository: { type: 'string', description: 'Repository slug containing the pull request.' },
 279 |               prId: { type: 'number', description: 'Pull request ID to comment on.' },
 280 |               text: { type: 'string', description: 'Comment text content. Supports Markdown formatting for code blocks, links, and emphasis.' },
 281 |               parentId: { type: 'number', description: 'ID of parent comment to reply to. Omit for top-level comments.' },
 282 |               filePath: { type: 'string', description: 'Path to the file in the repository where the comment should be added (e.g., "src/main.py", "README.md").' },
 283 |               line: { type: 'number', description: 'Line number in the file to attach the comment to (1-based).' },
 284 |               lineType: { type: 'string', enum: ['ADDED', 'REMOVED'], description: 'Type of change the comment is associated with: ADDED for additions, REMOVED for deletions.' }
 285 |             },
 286 |             required: ['repository', 'prId', 'text', 'filePath', 'line', 'lineType']
 287 |           }
 288 |         },
 289 |         {
 290 |           name: 'get_diff',
 291 |           description: 'Retrieve the code differences (diff) for a pull request showing what lines were added, removed, or modified. Use this to understand the scope of changes, review specific code modifications, or analyze the impact of proposed changes before merging.',
 292 |           inputSchema: {
 293 |             type: 'object',
 294 |             properties: {
 295 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 296 |               repository: { type: 'string', description: 'Repository slug containing the pull request.' },
 297 |               prId: { type: 'number', description: 'Pull request ID to get diff for.' },
 298 |               contextLines: { type: 'number', description: 'Number of context lines to show around changes (default: 10). Higher values provide more surrounding code context.' },
 299 |               maxLinesPerFile: { type: 'number', description: 'Maximum number of lines to show per file (default: uses BITBUCKET_DIFF_MAX_LINES_PER_FILE env var). Set to 0 for no limit. Prevents large files from overwhelming the diff output.' }
 300 |             },
 301 |             required: ['repository', 'prId']
 302 |           }
 303 |         },
 304 |         {
 305 |           name: 'get_reviews',
 306 |           description: 'Fetch the review history and approval status of a pull request. Use this to check who has reviewed the PR, see approval status, understand review feedback, or determine if the PR is ready for merging based on review requirements.',
 307 |           inputSchema: {
 308 |             type: 'object',
 309 |             properties: {
 310 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 311 |               repository: { type: 'string', description: 'Repository slug containing the pull request.' },
 312 |               prId: { type: 'number', description: 'Pull request ID to get reviews for.' }
 313 |             },
 314 |             required: ['repository', 'prId']
 315 |           }
 316 |         },
 317 |         {
 318 |           name: 'get_activities',
 319 |           description: 'Retrieve all activities for a pull request including comments, reviews, commits, and other timeline events. Use this to get the complete activity history and timeline of the pull request.',
 320 |           inputSchema: {
 321 |             type: 'object',
 322 |             properties: {
 323 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 324 |               repository: { type: 'string', description: 'Repository slug containing the pull request.' },
 325 |               prId: { type: 'number', description: 'Pull request ID to get activities for.' }
 326 |             },
 327 |             required: ['repository', 'prId']
 328 |           }
 329 |         },
 330 |         {
 331 |           name: 'get_comments',
 332 |           description: 'Retrieve only the comments from a pull request. Use this when you specifically want to read the discussion and feedback comments without other activities like reviews or commits.',
 333 |           inputSchema: {
 334 |             type: 'object',
 335 |             properties: {
 336 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 337 |               repository: { type: 'string', description: 'Repository slug containing the pull request.' },
 338 |               prId: { type: 'number', description: 'Pull request ID to get comments for.' }
 339 |             },
 340 |             required: ['repository', 'prId']
 341 |           }
 342 |         },
 343 |         {
 344 |           name: 'search',
 345 |           description: 'Search for code or files across repositories. Use this to find specific code patterns, file names, or content within projects and repositories. Searches both file contents and filenames. Supports filtering by project, repository, and query optimization.',
 346 |           inputSchema: {
 347 |             type: 'object',
 348 |             properties: {
 349 |               query: { type: 'string', description: 'Search query string to look for in code or file names.' },
 350 |               project: { type: 'string', description: 'Bitbucket project key to limit search scope. If omitted, searches across accessible projects.' },
 351 |               repository: { type: 'string', description: 'Repository slug to limit search to a specific repository within the project.' },
 352 |               type: { 
 353 |                 type: 'string', 
 354 |                 enum: ['code', 'file'],
 355 |                 description: 'Query optimization: "file" wraps query in quotes for exact filename matching, "code" uses default search behavior. Both search file contents and filenames.'
 356 |               },
 357 |               limit: { type: 'number', description: 'Number of results to return (default: 25, max: 100)' },
 358 |               start: { type: 'number', description: 'Start index for pagination (default: 0)' }
 359 |             },
 360 |             required: ['query']
 361 |           }
 362 |         },
 363 |         {
 364 |           name: 'get_file_content',
 365 |           description: 'Retrieve the content of a specific file from a Bitbucket repository with pagination support. Use this to read source code, configuration files, documentation, or any text-based files. For large files, use start parameter to paginate through content.',
 366 |           inputSchema: {
 367 |             type: 'object',
 368 |             properties: {
 369 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 370 |               repository: { type: 'string', description: 'Repository slug containing the file.' },
 371 |               filePath: { type: 'string', description: 'Path to the file in the repository (e.g., "src/main.py", "README.md", "config/settings.json").' },
 372 |               branch: { type: 'string', description: 'Branch or commit hash to read from (defaults to main/master branch if not specified).' },
 373 |               limit: { type: 'number', description: 'Maximum number of lines to return per request (default: 100, max: 1000).' },
 374 |               start: { type: 'number', description: 'Starting line number for pagination (0-based, default: 0).' }
 375 |             },
 376 |             required: ['repository', 'filePath']
 377 |           }
 378 |         },
 379 |         {
 380 |           name: 'browse_repository',
 381 |           description: 'Browse and list files and directories in a Bitbucket repository. Use this to explore repository structure, find files, or navigate directories.',
 382 |           inputSchema: {
 383 |             type: 'object',
 384 |             properties: {
 385 |               project: { type: 'string', description: 'Bitbucket project key. If omitted, uses BITBUCKET_DEFAULT_PROJECT environment variable.' },
 386 |               repository: { type: 'string', description: 'Repository slug to browse.' },
 387 |               path: { type: 'string', description: 'Directory path to browse (empty or "/" for root directory).' },
 388 |               branch: { type: 'string', description: 'Branch or commit hash to browse (defaults to main/master branch if not specified).' },
 389 |               limit: { type: 'number', description: 'Maximum number of items to return (default: 50).' }
 390 |             },
 391 |             required: ['repository']
 392 |           }
 393 |         }
 394 |       ].filter(tool => !this.config.readOnly || readOnlyTools.includes(tool.name))
 395 |     }));
 396 | 
 397 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
 398 |       try {
 399 |         logger.info(`Called tool: ${request.params.name}`, { arguments: request.params.arguments });
 400 |         const args = request.params.arguments ?? {};
 401 | 
 402 |         // Check if tool is allowed in read-only mode
 403 |         if (this.config.readOnly && !readOnlyTools.includes(request.params.name)) {
 404 |           throw new McpError(
 405 |             ErrorCode.MethodNotFound,
 406 |             `Tool ${request.params.name} is not available in read-only mode`
 407 |           );
 408 |         }
 409 | 
 410 |         // Helper function to get project with fallback to default
 411 |         const getProject = (providedProject?: string): string => {
 412 |           const project = providedProject || this.config.defaultProject;
 413 |           if (!project) {
 414 |             throw new McpError(
 415 |               ErrorCode.InvalidParams,
 416 |               'Project must be provided either as a parameter or through BITBUCKET_DEFAULT_PROJECT environment variable'
 417 |             );
 418 |           }
 419 |           return project;
 420 |         };
 421 | 
 422 |         switch (request.params.name) {
 423 |           case 'list_projects': {
 424 |             return await this.listProjects({
 425 |               limit: args.limit as number,
 426 |               start: args.start as number
 427 |             });
 428 |           }
 429 | 
 430 |           case 'list_repositories': {
 431 |             return await this.listRepositories({
 432 |               project: args.project as string,
 433 |               limit: args.limit as number,
 434 |               start: args.start as number
 435 |             });
 436 |           }
 437 | 
 438 |           case 'create_pull_request': {
 439 |             if (!this.isPullRequestInput(args)) {
 440 |               throw new McpError(
 441 |                 ErrorCode.InvalidParams,
 442 |                 'Invalid pull request input parameters'
 443 |               );
 444 |             }
 445 |             // Ensure project is set
 446 |             const createArgs = { ...args, project: getProject(args.project) };
 447 |             return await this.createPullRequest(createArgs);
 448 |           }
 449 | 
 450 |           case 'get_pull_request': {
 451 |             const getPrParams: PullRequestParams = {
 452 |               project: getProject(args.project as string),
 453 |               repository: args.repository as string,
 454 |               prId: args.prId as number
 455 |             };
 456 |             return await this.getPullRequest(getPrParams);
 457 |           }
 458 | 
 459 |           case 'merge_pull_request': {
 460 |             const mergePrParams: PullRequestParams = {
 461 |               project: getProject(args.project as string),
 462 |               repository: args.repository as string,
 463 |               prId: args.prId as number
 464 |             };
 465 |             return await this.mergePullRequest(mergePrParams, {
 466 |               message: args.message as string,
 467 |               strategy: args.strategy as 'merge-commit' | 'squash' | 'fast-forward'
 468 |             });
 469 |           }
 470 | 
 471 |           case 'decline_pull_request': {
 472 |             const declinePrParams: PullRequestParams = {
 473 |               project: getProject(args.project as string),
 474 |               repository: args.repository as string,
 475 |               prId: args.prId as number
 476 |             };
 477 |             return await this.declinePullRequest(declinePrParams, args.message as string);
 478 |           }
 479 | 
 480 |           case 'add_comment': {
 481 |             const commentPrParams: PullRequestParams = {
 482 |               project: getProject(args.project as string),
 483 |               repository: args.repository as string,
 484 |               prId: args.prId as number
 485 |             };
 486 |             return await this.addComment(commentPrParams, {
 487 |               text: args.text as string,
 488 |               parentId: args.parentId as number
 489 |             });
 490 |           }
 491 | 
 492 |            case 'add_comment_inline': {
 493 |             const commentPrParams: PullRequestParams = {
 494 |               project: getProject(args.project as string),
 495 |               repository: args.repository as string,
 496 |               prId: args.prId as number
 497 |             };
 498 |             return await this.addCommentInline(commentPrParams, {
 499 |               text: args.text as string,
 500 |               parentId: args.parentId as number,
 501 |               filePath: args.filePath as string,
 502 |               line: args.line as number,
 503 |               lineType: args.lineType as 'ADDED' | 'REMOVED'
 504 |             });
 505 |           }
 506 | 
 507 |           case 'get_diff': {
 508 |             const diffPrParams: PullRequestParams = {
 509 |               project: getProject(args.project as string),
 510 |               repository: args.repository as string,
 511 |               prId: args.prId as number
 512 |             };
 513 |             return await this.getDiff(
 514 |               diffPrParams, 
 515 |               args.contextLines as number, 
 516 |               args.maxLinesPerFile as number
 517 |             );
 518 |           }
 519 | 
 520 |           case 'get_reviews': {
 521 |             const reviewsPrParams: PullRequestParams = {
 522 |               project: getProject(args.project as string),
 523 |               repository: args.repository as string,
 524 |               prId: args.prId as number
 525 |             };
 526 |             return await this.getReviews(reviewsPrParams);
 527 |           }
 528 | 
 529 |           case 'get_activities': {
 530 |             const activitiesPrParams: PullRequestParams = {
 531 |               project: getProject(args.project as string),
 532 |               repository: args.repository as string,
 533 |               prId: args.prId as number
 534 |             };
 535 |             return await this.getActivities(activitiesPrParams);
 536 |           }
 537 | 
 538 |           case 'get_comments': {
 539 |             const commentsPrParams: PullRequestParams = {
 540 |               project: getProject(args.project as string),
 541 |               repository: args.repository as string,
 542 |               prId: args.prId as number
 543 |             };
 544 |             return await this.getComments(commentsPrParams);
 545 |           }
 546 | 
 547 |           case 'search': {
 548 |             return await this.search({
 549 |               query: args.query as string,
 550 |               project: args.project as string,
 551 |               repository: args.repository as string,
 552 |               type: args.type as 'code' | 'file',
 553 |               limit: args.limit as number,
 554 |               start: args.start as number
 555 |             });
 556 |           }
 557 | 
 558 |           case 'get_file_content': {
 559 |             return await this.getFileContent({
 560 |               project: getProject(args.project as string),
 561 |               repository: args.repository as string,
 562 |               filePath: args.filePath as string,
 563 |               branch: args.branch as string,
 564 |               limit: args.limit as number,
 565 |               start: args.start as number
 566 |             });
 567 |           }
 568 | 
 569 |           case 'browse_repository': {
 570 |             return await this.browseRepository({
 571 |               project: getProject(args.project as string),
 572 |               repository: args.repository as string,
 573 |               path: args.path as string,
 574 |               branch: args.branch as string,
 575 |               limit: args.limit as number
 576 |             });
 577 |           }
 578 | 
 579 |           default:
 580 |             throw new McpError(
 581 |               ErrorCode.MethodNotFound,
 582 |               `Unknown tool: ${request.params.name}`
 583 |             );
 584 |         }
 585 |       } catch (error) {
 586 |         logger.error('Tool execution error', { error });
 587 |         if (axios.isAxiosError(error)) {
 588 |           throw new McpError(
 589 |             ErrorCode.InternalError,
 590 |             `Bitbucket API error: ${error.response?.data.message ?? error.message}`
 591 |           );
 592 |         }
 593 |         throw error;
 594 |       }
 595 |     });
 596 |   }
 597 | 
 598 |   private async listProjects(options: ListOptions = {}) {
 599 |     const { limit = 25, start = 0 } = options;
 600 |     const response = await this.api.get('/projects', {
 601 |       params: { limit, start }
 602 |     });
 603 | 
 604 |     const projects = response.data.values || [];
 605 |     const summary = {
 606 |       total: response.data.size || projects.length,
 607 |       showing: projects.length,
 608 |       projects: projects.map((project: { key: string; name: string; description?: string; public: boolean; type: string }) => ({
 609 |         key: project.key,
 610 |         name: project.name,
 611 |         description: project.description,
 612 |         public: project.public,
 613 |         type: project.type
 614 |       }))
 615 |     };
 616 | 
 617 |     return {
 618 |       content: [{ 
 619 |         type: 'text', 
 620 |         text: JSON.stringify(summary, null, 2) 
 621 |       }]
 622 |     };
 623 |   }
 624 | 
 625 |   private async listRepositories(options: ListRepositoriesOptions = {}) {
 626 |     const { project, limit = 25, start = 0 } = options;
 627 |     
 628 |     let endpoint: string;
 629 |     const params = { limit, start };
 630 | 
 631 |     if (project || this.config.defaultProject) {
 632 |       // List repositories for a specific project
 633 |       const projectKey = project || this.config.defaultProject;
 634 |       endpoint = `/projects/${projectKey}/repos`;
 635 |     } else {
 636 |       // List all accessible repositories
 637 |       endpoint = '/repos';
 638 |     }
 639 | 
 640 |     const response = await this.api.get(endpoint, { params });
 641 | 
 642 |     const repositories = response.data.values || [];
 643 |     const summary = {
 644 |       project: project || this.config.defaultProject || 'all',
 645 |       total: response.data.size || repositories.length,
 646 |       showing: repositories.length,
 647 |       repositories: repositories.map((repo: { 
 648 |         slug: string; 
 649 |         name: string; 
 650 |         description?: string; 
 651 |         project?: { key: string }; 
 652 |         public: boolean; 
 653 |         links?: { clone?: { name: string; href: string }[] }; 
 654 |         state: string 
 655 |       }) => ({
 656 |         slug: repo.slug,
 657 |         name: repo.name,
 658 |         description: repo.description,
 659 |         project: repo.project?.key,
 660 |         public: repo.public,
 661 |         cloneUrl: repo.links?.clone?.find((link: { name: string; href: string }) => link.name === 'http')?.href,
 662 |         state: repo.state
 663 |       }))
 664 |     };
 665 | 
 666 |     return {
 667 |       content: [{ 
 668 |         type: 'text', 
 669 |         text: JSON.stringify(summary, null, 2) 
 670 |       }]
 671 |     };
 672 |   }
 673 | 
 674 |   private async createPullRequest(input: PullRequestInput) {
 675 |     const response = await this.api.post(
 676 |       `/projects/${input.project}/repos/${input.repository}/pull-requests`,
 677 |       {
 678 |         title: input.title,
 679 |         description: input.description,
 680 |         fromRef: {
 681 |           id: `refs/heads/${input.sourceBranch}`,
 682 |           repository: {
 683 |             slug: input.repository,
 684 |             project: { key: input.project }
 685 |           }
 686 |         },
 687 |         toRef: {
 688 |           id: `refs/heads/${input.targetBranch}`,
 689 |           repository: {
 690 |             slug: input.repository,
 691 |             project: { key: input.project }
 692 |           }
 693 |         },
 694 |         reviewers: input.reviewers?.map(username => ({ user: { name: username } }))
 695 |       }
 696 |     );
 697 | 
 698 |     return {
 699 |       content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }]
 700 |     };
 701 |   }
 702 | 
 703 |   private async getPullRequest(params: PullRequestParams) {
 704 |     const { project, repository, prId } = params;
 705 |     
 706 |     if (!project || !repository || !prId) {
 707 |       throw new McpError(
 708 |         ErrorCode.InvalidParams,
 709 |         'Project, repository, and prId are required'
 710 |       );
 711 |     }
 712 |     
 713 |     const response = await this.api.get(
 714 |       `/projects/${project}/repos/${repository}/pull-requests/${prId}`
 715 |     );
 716 | 
 717 |     return {
 718 |       content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }]
 719 |     };
 720 |   }
 721 | 
 722 |   private async mergePullRequest(params: PullRequestParams, options: MergeOptions = {}) {
 723 |     const { project, repository, prId } = params;
 724 |     
 725 |     if (!project || !repository || !prId) {
 726 |       throw new McpError(
 727 |         ErrorCode.InvalidParams,
 728 |         'Project, repository, and prId are required'
 729 |       );
 730 |     }
 731 |     
 732 |     const { message, strategy = 'merge-commit' } = options;
 733 |     
 734 |     const response = await this.api.post(
 735 |       `/projects/${project}/repos/${repository}/pull-requests/${prId}/merge`,
 736 |       {
 737 |         version: -1,
 738 |         message,
 739 |         strategy
 740 |       }
 741 |     );
 742 | 
 743 |     return {
 744 |       content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }]
 745 |     };
 746 |   }
 747 | 
 748 |   private async declinePullRequest(params: PullRequestParams, message?: string) {
 749 |     const { project, repository, prId } = params;
 750 |     
 751 |     if (!project || !repository || !prId) {
 752 |       throw new McpError(
 753 |         ErrorCode.InvalidParams,
 754 |         'Project, repository, and prId are required'
 755 |       );
 756 |     }
 757 |     
 758 |     const response = await this.api.post(
 759 |       `/projects/${project}/repos/${repository}/pull-requests/${prId}/decline`,
 760 |       {
 761 |         version: -1,
 762 |         message
 763 |       }
 764 |     );
 765 | 
 766 |     return {
 767 |       content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }]
 768 |     };
 769 |   }
 770 | 
 771 |   private async addComment(params: PullRequestParams, options: CommentOptions) {
 772 |     const { project, repository, prId } = params;
 773 |     
 774 |     if (!project || !repository || !prId) {
 775 |       throw new McpError(
 776 |         ErrorCode.InvalidParams,
 777 |         'Project, repository, and prId are required'
 778 |       );
 779 |     }
 780 |     
 781 |     const { text, parentId } = options;
 782 |     
 783 |     const response = await this.api.post(
 784 |       `/projects/${project}/repos/${repository}/pull-requests/${prId}/comments`,
 785 |       {
 786 |         text,
 787 |         parent: parentId ? { id: parentId } : undefined
 788 |       }
 789 |     );
 790 | 
 791 |     return {
 792 |       content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }]
 793 |     };
 794 |   }
 795 | 
 796 |   private async addCommentInline(params: PullRequestParams, options: InlineCommentOptions) {
 797 |     const { project, repository, prId } = params;
 798 |     
 799 |     if (!project || !repository || !prId || !options.filePath || !options.line || !options.lineType) {
 800 |       throw new McpError(
 801 |         ErrorCode.InvalidParams,
 802 |         'Project, repository, prId, filePath, line, and lineType are required'
 803 |       );
 804 |     }
 805 |     
 806 |     const { text, parentId } = options;
 807 |     
 808 |     const response = await this.api.post(
 809 |       `/projects/${project}/repos/${repository}/pull-requests/${prId}/comments`,
 810 |       {
 811 |         text,
 812 |         parent: parentId ? { id: parentId } : undefined,
 813 |         anchor: {
 814 |           path: options.filePath,
 815 |           lineType: options.lineType,
 816 |           line: options.line,
 817 |           diffType: 'EFFECTIVE',
 818 |           fileType: 'TO',}
 819 |       }
 820 |     );
 821 | 
 822 |     logger.error(response);
 823 | 
 824 |     return {
 825 |       content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }]
 826 |     };
 827 |   }
 828 | 
 829 |   private truncateDiff(diffContent: string, maxLinesPerFile: number): string {
 830 |     if (!maxLinesPerFile || maxLinesPerFile <= 0) {
 831 |       return diffContent;
 832 |     }
 833 | 
 834 |     const lines = diffContent.split('\n');
 835 |     const result: string[] = [];
 836 |     let currentFileLines: string[] = [];
 837 |     let currentFileName = '';
 838 |     let inFileContent = false;
 839 | 
 840 |     for (const line of lines) {
 841 |       // Detect file headers (diff --git, index, +++, ---)
 842 |       if (line.startsWith('diff --git ')) {
 843 |         // Process previous file if any
 844 |         if (currentFileLines.length > 0) {
 845 |           result.push(...this.truncateFileSection(currentFileLines, currentFileName, maxLinesPerFile));
 846 |           currentFileLines = [];
 847 |         }
 848 |         
 849 |         // Extract filename for context
 850 |         const match = line.match(/diff --git a\/(.+) b\/(.+)/);
 851 |         currentFileName = match ? match[2] : 'unknown';
 852 |         inFileContent = false;
 853 |         
 854 |         // Always include file headers
 855 |         result.push(line);
 856 |       } else if (line.startsWith('index ') || line.startsWith('+++') || line.startsWith('---')) {
 857 |         // Always include file metadata
 858 |         result.push(line);
 859 |       } else if (line.startsWith('@@')) {
 860 |         // Hunk header - marks start of actual file content
 861 |         inFileContent = true;
 862 |         currentFileLines.push(line);
 863 |       } else if (inFileContent) {
 864 |         // Collect file content lines for potential truncation
 865 |         currentFileLines.push(line);
 866 |       } else {
 867 |         // Other lines (empty lines between files, etc.)
 868 |         result.push(line);
 869 |       }
 870 |     }
 871 | 
 872 |     // Process the last file
 873 |     if (currentFileLines.length > 0) {
 874 |       result.push(...this.truncateFileSection(currentFileLines, currentFileName, maxLinesPerFile));
 875 |     }
 876 | 
 877 |     return result.join('\n');
 878 |   }
 879 | 
 880 |   private truncateFileSection(fileLines: string[], fileName: string, maxLines: number): string[] {
 881 |     if (fileLines.length <= maxLines) {
 882 |       return fileLines;
 883 |     }
 884 | 
 885 |     // Count actual content lines (excluding hunk headers)
 886 |     const contentLines = fileLines.filter(line => !line.startsWith('@@'));
 887 |     const hunkHeaders = fileLines.filter(line => line.startsWith('@@'));
 888 | 
 889 |     if (contentLines.length <= maxLines) {
 890 |       return fileLines; // No need to truncate if content is within limit
 891 |     }
 892 | 
 893 |     // Smart truncation: show beginning and end
 894 |     const showAtStart = Math.floor(maxLines * 0.6); // 60% at start
 895 |     const showAtEnd = Math.floor(maxLines * 0.4);   // 40% at end
 896 |     const truncatedCount = contentLines.length - showAtStart - showAtEnd;
 897 | 
 898 |     const result: string[] = [];
 899 |     
 900 |     // Add hunk headers first
 901 |     result.push(...hunkHeaders);
 902 |     
 903 |     // Add first portion
 904 |     result.push(...contentLines.slice(0, showAtStart));
 905 |     
 906 |     // Add truncation message
 907 |     result.push('');
 908 |     result.push(`[*** FILE TRUNCATED: ${truncatedCount} lines hidden from ${fileName} ***]`);
 909 |     result.push(`[*** File had ${contentLines.length} total lines, showing first ${showAtStart} and last ${showAtEnd} ***]`);
 910 |     result.push(`[*** Use maxLinesPerFile=0 to see complete diff ***]`);
 911 |     result.push('');
 912 |     
 913 |     // Add last portion
 914 |     result.push(...contentLines.slice(-showAtEnd));
 915 | 
 916 |     return result;
 917 |   }
 918 | 
 919 |   private async getDiff(params: PullRequestParams, contextLines: number = 10, maxLinesPerFile?: number) {
 920 |     const { project, repository, prId } = params;
 921 |     
 922 |     if (!project || !repository || !prId) {
 923 |       throw new McpError(
 924 |         ErrorCode.InvalidParams,
 925 |         'Project, repository, and prId are required'
 926 |       );
 927 |     }
 928 |     
 929 |     const response = await this.api.get(
 930 |       `/projects/${project}/repos/${repository}/pull-requests/${prId}/diff`,
 931 |       {
 932 |         params: { contextLines },
 933 |         headers: { Accept: 'text/plain' }
 934 |       }
 935 |     );
 936 | 
 937 |     // Determine max lines per file: parameter > env var > no limit
 938 |     const effectiveMaxLines = maxLinesPerFile !== undefined 
 939 |       ? maxLinesPerFile 
 940 |       : this.config.maxLinesPerFile;
 941 | 
 942 |     const diffContent = effectiveMaxLines 
 943 |       ? this.truncateDiff(response.data, effectiveMaxLines)
 944 |       : response.data;
 945 | 
 946 |     return {
 947 |       content: [{ type: 'text', text: diffContent }]
 948 |     };
 949 |   }
 950 | 
 951 |   private async getReviews(params: PullRequestParams) {
 952 |     const { project, repository, prId } = params;
 953 |     
 954 |     if (!project || !repository || !prId) {
 955 |       throw new McpError(
 956 |         ErrorCode.InvalidParams,
 957 |         'Project, repository, and prId are required'
 958 |       );
 959 |     }
 960 |     
 961 |     const response = await this.api.get(
 962 |       `/projects/${project}/repos/${repository}/pull-requests/${prId}/activities`
 963 |     );
 964 | 
 965 |     const reviews = response.data.values.filter(
 966 |       (activity: BitbucketActivity) => activity.action === 'APPROVED' || activity.action === 'REVIEWED'
 967 |     );
 968 | 
 969 |     return {
 970 |       content: [{ type: 'text', text: JSON.stringify(reviews, null, 2) }]
 971 |     };
 972 |   }
 973 | 
 974 |   private async getActivities(params: PullRequestParams) {
 975 |     const { project, repository, prId } = params;
 976 |     
 977 |     if (!project || !repository || !prId) {
 978 |       throw new McpError(
 979 |         ErrorCode.InvalidParams,
 980 |         'Project, repository, and prId are required'
 981 |       );
 982 |     }
 983 |     
 984 |     const response = await this.api.get(
 985 |       `/projects/${project}/repos/${repository}/pull-requests/${prId}/activities`
 986 |     );
 987 | 
 988 |     return {
 989 |       content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }]
 990 |     };
 991 |   }
 992 | 
 993 |   private async getComments(params: PullRequestParams) {
 994 |     const { project, repository, prId } = params;
 995 |     
 996 |     if (!project || !repository || !prId) {
 997 |       throw new McpError(
 998 |         ErrorCode.InvalidParams,
 999 |         'Project, repository, and prId are required'
1000 |       );
1001 |     }
1002 |     
1003 |     const response = await this.api.get(
1004 |       `/projects/${project}/repos/${repository}/pull-requests/${prId}/activities`
1005 |     );
1006 | 
1007 |     const comments = response.data.values.filter(
1008 |       (activity: BitbucketActivity) => activity.action === 'COMMENTED'
1009 |     );
1010 | 
1011 |     return {
1012 |       content: [{ type: 'text', text: JSON.stringify(comments, null, 2) }]
1013 |     };
1014 |   }
1015 | 
1016 |   private async search(options: SearchOptions) {
1017 |     const { query, project, repository, type, limit = 25, start = 0 } = options;
1018 |     
1019 |     if (!query) {
1020 |       throw new McpError(
1021 |         ErrorCode.InvalidParams,
1022 |         'Query parameter is required'
1023 |       );
1024 |     }
1025 | 
1026 |     // Build the search query with filters
1027 |     let searchQuery = query;
1028 |     
1029 |     // Add project filter if specified
1030 |     if (project) {
1031 |       searchQuery = `${searchQuery} project:${project}`;
1032 |     }
1033 |     
1034 |     // Add repository filter if specified (requires project)
1035 |     if (repository && project) {
1036 |       searchQuery = `${searchQuery} repo:${project}/${repository}`;
1037 |     }
1038 |     
1039 |     // Add file extension filter if type is specified
1040 |     if (type === 'file') {
1041 |       // For file searches, wrap query in quotes for exact filename matching
1042 |       if (!query.includes('ext:') && !query.startsWith('"')) {
1043 |         searchQuery = `"${query}"`;
1044 |         if (project) searchQuery += ` project:${project}`;
1045 |         if (repository && project) searchQuery += ` repo:${project}/${repository}`;
1046 |       }
1047 |     } else if (type === 'code' && !query.includes('ext:')) {
1048 |       // For code searches, add common extension filters if not specified
1049 |       // This can be enhanced based on user needs
1050 |     }
1051 | 
1052 |     const requestBody = {
1053 |       query: searchQuery,
1054 |       entities: {
1055 |         code: {
1056 |           start,
1057 |           limit: Math.min(limit, 100)
1058 |         }
1059 |       }
1060 |     };
1061 | 
1062 |     try {
1063 |       // Use full URL for search API since it uses different base path
1064 |       const searchUrl = `${this.config.baseUrl}/rest/search/latest/search`;
1065 |       const response = await axios.post(searchUrl, requestBody, {
1066 |         headers: this.config.token
1067 |           ? { 
1068 |               Authorization: `Bearer ${this.config.token}`,
1069 |               'Content-Type': 'application/json'
1070 |             }
1071 |           : { 'Content-Type': 'application/json' },
1072 |         auth: this.config.username && this.config.password
1073 |           ? { username: this.config.username, password: this.config.password }
1074 |           : undefined,
1075 |       });
1076 |       
1077 |       const codeResults = response.data.code || {};
1078 |       const searchResults = {
1079 |         query: searchQuery,
1080 |         originalQuery: query,
1081 |         project: project || 'global',
1082 |         repository: repository || 'all',
1083 |         type: type || 'code',
1084 |         scope: response.data.scope || {},
1085 |         total: codeResults.count || 0,
1086 |         showing: codeResults.values?.length || 0,
1087 |         isLastPage: codeResults.isLastPage || true,
1088 |         nextStart: codeResults.nextStart || null,
1089 |         results: codeResults.values?.map((result: any) => ({
1090 |           repository: result.repository,
1091 |           file: result.file,
1092 |           hitCount: result.hitCount || 0,
1093 |           pathMatches: result.pathMatches || [],
1094 |           hitContexts: result.hitContexts || []
1095 |         })) || []
1096 |       };
1097 | 
1098 |       return {
1099 |         content: [{ type: 'text', text: JSON.stringify(searchResults, null, 2) }]
1100 |       };
1101 |     } catch (error) {
1102 |       if (axios.isAxiosError(error)) {
1103 |         if (error.response?.status === 404) {
1104 |           throw new McpError(
1105 |             ErrorCode.InternalError,
1106 |             'Search API endpoint not available on this Bitbucket instance'
1107 |           );
1108 |         }
1109 |         // Handle specific search API errors
1110 |         const errorData = error.response?.data;
1111 |         if (errorData?.errors && errorData.errors.length > 0) {
1112 |           const firstError = errorData.errors[0];
1113 |           throw new McpError(
1114 |             ErrorCode.InvalidParams,
1115 |             `Search error: ${firstError.message || 'Invalid search query'}`
1116 |           );
1117 |         }
1118 |       }
1119 |       throw error;
1120 |     }
1121 |   }
1122 | 
1123 |   private async getFileContent(options: FileContentOptions) {
1124 |     const { project, repository, filePath, branch, limit = 100, start = 0 } = options;
1125 |     
1126 |     if (!project || !repository || !filePath) {
1127 |       throw new McpError(
1128 |         ErrorCode.InvalidParams,
1129 |         'Project, repository, and filePath are required'
1130 |       );
1131 |     }
1132 | 
1133 |     const params: Record<string, string | number> = {
1134 |       limit: Math.min(limit, 1000),
1135 |       start
1136 |     };
1137 | 
1138 |     if (branch) {
1139 |       params.at = branch;
1140 |     }
1141 | 
1142 |     const response = await this.api.get(
1143 |       `/projects/${project}/repos/${repository}/browse/${filePath}`,
1144 |       { params }
1145 |     );
1146 | 
1147 |     const fileContent = {
1148 |       project,
1149 |       repository,
1150 |       filePath,
1151 |       branch: branch || 'default',
1152 |       isLastPage: response.data.isLastPage,
1153 |       size: response.data.size,
1154 |       showing: response.data.lines?.length || 0,
1155 |       startLine: start,
1156 |       lines: response.data.lines?.map((line: { text: string }) => line.text) || []
1157 |     };
1158 | 
1159 |     return {
1160 |       content: [{ type: 'text', text: JSON.stringify(fileContent, null, 2) }]
1161 |     };
1162 |   }
1163 | 
1164 |   private async browseRepository(options: { project: string; repository: string; path?: string; branch?: string; limit?: number }) {
1165 |     const { project, repository, path = '', branch, limit = 50 } = options;
1166 |     
1167 |     if (!project || !repository) {
1168 |       throw new McpError(
1169 |         ErrorCode.InvalidParams,
1170 |         'Project and repository are required'
1171 |       );
1172 |     }
1173 | 
1174 |     const params: Record<string, string | number> = {
1175 |       limit
1176 |     };
1177 | 
1178 |     if (branch) {
1179 |       params.at = branch;
1180 |     }
1181 | 
1182 |     const browsePath = path ? `/${path}` : '';
1183 |     const response = await this.api.get(
1184 |       `/projects/${project}/repos/${repository}/browse${browsePath}`,
1185 |       { params }
1186 |     );
1187 | 
1188 |     const children = response.data.children || {};
1189 |     const browseResults = {
1190 |       project,
1191 |       repository,
1192 |       path: path || 'root',
1193 |       branch: branch || response.data.revision || 'default',
1194 |       isLastPage: children.isLastPage || false,
1195 |       size: children.size || 0,
1196 |       showing: children.values?.length || 0,
1197 |       items: children.values?.map((item: { 
1198 |         path: { name: string; toString: string }; 
1199 |         type: string; 
1200 |         size?: number 
1201 |       }) => ({
1202 |         name: item.path.name,
1203 |         path: item.path.toString,
1204 |         type: item.type,
1205 |         size: item.size
1206 |       })) || []
1207 |     };
1208 | 
1209 |     return {
1210 |       content: [{ type: 'text', text: JSON.stringify(browseResults, null, 2) }]
1211 |     };
1212 |   }
1213 | 
1214 |   async run() {
1215 |     const transport = new StdioServerTransport();
1216 |     await this.server.connect(transport);
1217 |     logger.info('Bitbucket MCP server running on stdio');
1218 |   }
1219 | }
1220 | 
1221 | const server = new BitbucketServer();
1222 | server.run().catch((error) => {
1223 |   logger.error('Server error', error);
1224 |   process.exit(1);
1225 | });
```