#
tokens: 48327/50000 59/114 files (page 1/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 6. Use http://codebase.md/aashari/mcp-server-atlassian-bitbucket?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .github
│   ├── dependabot.yml
│   └── workflows
│       ├── ci-dependabot-auto-merge.yml
│       ├── ci-dependency-check.yml
│       └── ci-semantic-release.yml
├── .gitignore
├── .gitkeep
├── .npmignore
├── .npmrc
├── .prettierrc
├── .releaserc.json
├── .trigger-ci
├── CHANGELOG.md
├── eslint.config.mjs
├── jest.setup.js
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── ensure-executable.js
│   ├── package.json
│   └── update-version.js
├── src
│   ├── cli
│   │   ├── atlassian.diff.cli.ts
│   │   ├── atlassian.pullrequests.cli.test.ts
│   │   ├── atlassian.pullrequests.cli.ts
│   │   ├── atlassian.repositories.cli.test.ts
│   │   ├── atlassian.repositories.cli.ts
│   │   ├── atlassian.search.cli.test.ts
│   │   ├── atlassian.search.cli.ts
│   │   ├── atlassian.workspaces.cli.test.ts
│   │   ├── atlassian.workspaces.cli.ts
│   │   └── index.ts
│   ├── controllers
│   │   ├── atlassian.diff.controller.ts
│   │   ├── atlassian.diff.formatter.ts
│   │   ├── atlassian.pullrequests.approve.controller.ts
│   │   ├── atlassian.pullrequests.base.controller.ts
│   │   ├── atlassian.pullrequests.comments.controller.ts
│   │   ├── atlassian.pullrequests.controller.test.ts
│   │   ├── atlassian.pullrequests.controller.ts
│   │   ├── atlassian.pullrequests.create.controller.ts
│   │   ├── atlassian.pullrequests.formatter.ts
│   │   ├── atlassian.pullrequests.get.controller.ts
│   │   ├── atlassian.pullrequests.list.controller.ts
│   │   ├── atlassian.pullrequests.reject.controller.ts
│   │   ├── atlassian.pullrequests.update.controller.ts
│   │   ├── atlassian.repositories.branch.controller.ts
│   │   ├── atlassian.repositories.commit.controller.ts
│   │   ├── atlassian.repositories.content.controller.ts
│   │   ├── atlassian.repositories.controller.test.ts
│   │   ├── atlassian.repositories.details.controller.ts
│   │   ├── atlassian.repositories.formatter.ts
│   │   ├── atlassian.repositories.list.controller.ts
│   │   ├── atlassian.search.code.controller.ts
│   │   ├── atlassian.search.content.controller.ts
│   │   ├── atlassian.search.controller.test.ts
│   │   ├── atlassian.search.controller.ts
│   │   ├── atlassian.search.formatter.ts
│   │   ├── atlassian.search.pullrequests.controller.ts
│   │   ├── atlassian.search.repositories.controller.ts
│   │   ├── atlassian.workspaces.controller.test.ts
│   │   ├── atlassian.workspaces.controller.ts
│   │   └── atlassian.workspaces.formatter.ts
│   ├── index.ts
│   ├── services
│   │   ├── vendor.atlassian.pullrequests.service.ts
│   │   ├── vendor.atlassian.pullrequests.test.ts
│   │   ├── vendor.atlassian.pullrequests.types.ts
│   │   ├── vendor.atlassian.repositories.diff.service.ts
│   │   ├── vendor.atlassian.repositories.diff.types.ts
│   │   ├── vendor.atlassian.repositories.service.test.ts
│   │   ├── vendor.atlassian.repositories.service.ts
│   │   ├── vendor.atlassian.repositories.types.ts
│   │   ├── vendor.atlassian.search.service.ts
│   │   ├── vendor.atlassian.search.types.ts
│   │   ├── vendor.atlassian.workspaces.service.ts
│   │   ├── vendor.atlassian.workspaces.test.ts
│   │   └── vendor.atlassian.workspaces.types.ts
│   ├── tools
│   │   ├── atlassian.diff.tool.ts
│   │   ├── atlassian.diff.types.ts
│   │   ├── atlassian.pullrequests.tool.ts
│   │   ├── atlassian.pullrequests.types.test.ts
│   │   ├── atlassian.pullrequests.types.ts
│   │   ├── atlassian.repositories.tool.ts
│   │   ├── atlassian.repositories.types.ts
│   │   ├── atlassian.search.tool.ts
│   │   ├── atlassian.search.types.ts
│   │   ├── atlassian.workspaces.tool.ts
│   │   └── atlassian.workspaces.types.ts
│   ├── types
│   │   └── common.types.ts
│   └── utils
│       ├── adf.util.test.ts
│       ├── adf.util.ts
│       ├── atlassian.util.ts
│       ├── bitbucket-error-detection.test.ts
│       ├── cli.test.util.ts
│       ├── config.util.test.ts
│       ├── config.util.ts
│       ├── constants.util.ts
│       ├── defaults.util.ts
│       ├── diff.util.ts
│       ├── error-handler.util.test.ts
│       ├── error-handler.util.ts
│       ├── error.util.test.ts
│       ├── error.util.ts
│       ├── formatter.util.ts
│       ├── logger.util.ts
│       ├── markdown.util.test.ts
│       ├── markdown.util.ts
│       ├── pagination.util.ts
│       ├── path.util.test.ts
│       ├── path.util.ts
│       ├── query.util.ts
│       ├── shell.util.ts
│       ├── transport.util.test.ts
│       ├── transport.util.ts
│       └── workspace.util.ts
├── STYLE_GUIDE.md
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitkeep:
--------------------------------------------------------------------------------

```
1 | 
```

--------------------------------------------------------------------------------
/.trigger-ci:
--------------------------------------------------------------------------------

```
1 | # CI/CD trigger Thu Sep 18 00:41:08 WIB 2025
2 | 
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
1 | {
2 |   "singleQuote": true,
3 |   "semi": true,
4 |   "useTabs": true,
5 |   "tabWidth": 4,
6 |   "printWidth": 80,
7 |   "trailingComma": "all"
8 | } 
```

--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------

```
1 | # This file is for local development only
2 | # The CI/CD workflow will create its own .npmrc files
3 | 
4 | # For npm registry
5 | registry=https://registry.npmjs.org/
6 | 
7 | # GitHub Packages configuration removed
8 | 
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
 1 | # Source code
 2 | src/
 3 | *.ts
 4 | !*.d.ts
 5 | 
 6 | # Tests
 7 | *.test.ts
 8 | *.test.js
 9 | __tests__/
10 | coverage/
11 | jest.config.js
12 | 
13 | # Development files
14 | .github/
15 | .git/
16 | .gitignore
17 | .eslintrc
18 | .eslintrc.js
19 | .eslintignore
20 | .prettierrc
21 | .prettierrc.js
22 | tsconfig.json
23 | *.tsbuildinfo
24 | 
25 | # Editor directories
26 | .idea/
27 | .vscode/
28 | *.swp
29 | *.swo
30 | 
31 | # Logs
32 | logs
33 | *.log
34 | npm-debug.log*
35 | yarn-debug.log*
36 | yarn-error.log*
37 | 
38 | # CI/CD
39 | .travis.yml
40 | 
41 | # Runtime data
42 | .env
43 | .env.* 
```

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

```
 1 | # Dependency directories
 2 | node_modules/
 3 | .npm
 4 | 
 5 | # TypeScript output
 6 | dist/
 7 | build/
 8 | *.tsbuildinfo
 9 | 
10 | # Coverage directories
11 | coverage/
12 | .nyc_output/
13 | 
14 | # Environment variables
15 | .env
16 | .env.local
17 | .env.*.local
18 | 
19 | # Log files
20 | logs/
21 | *.log
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 | 
26 | # IDE files
27 | .idea/
28 | .vscode/
29 | *.sublime-project
30 | *.sublime-workspace
31 | .project
32 | .classpath
33 | .settings/
34 | .DS_Store
35 | 
36 | # Temp directories
37 | .tmp/
38 | temp/
39 | 
40 | # Backup files
41 | *.bak
42 | 
43 | # Editor directories and files
44 | *.suo
45 | *.ntvs*
46 | *.njsproj
47 | *.sln
48 | *.sw?
49 | 
50 | # macOS
51 | .DS_Store
52 | 
53 | # Misc
54 | .yarn-integrity
55 | 
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
 1 | # Enable debug logging
 2 | DEBUG=false
 3 | 
 4 | # Atlassian Configuration - Method 1 (Standard Atlassian - recommended)
 5 | # Use this for general Atlassian services (works with Bitbucket, Jira, Confluence)
 6 | ATLASSIAN_SITE_NAME=your-instance
 7 | [email protected]
 8 | ATLASSIAN_API_TOKEN=
 9 | 
10 | # Atlassian Configuration - Method 2 (Bitbucket-specific alternative)
11 | # Use this if you prefer Bitbucket username + app password authentication
12 | # ATLASSIAN_BITBUCKET_USERNAME=your-bitbucket-username
13 | # ATLASSIAN_BITBUCKET_APP_PASSWORD=your-app-password
14 | 
15 | # Optional: Default workspace for commands
16 | # BITBUCKET_DEFAULT_WORKSPACE=your-main-workspace-slug
17 | 
```

--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"branches": ["main"],
 3 | 	"plugins": [
 4 | 		"@semantic-release/commit-analyzer",
 5 | 		"@semantic-release/release-notes-generator",
 6 | 		"@semantic-release/changelog",
 7 | 		[
 8 | 			"@semantic-release/exec",
 9 | 			{
10 | 				"prepareCmd": "node scripts/update-version.js ${nextRelease.version} && npm run build && chmod +x dist/index.js"
11 | 			}
12 | 		],
13 | 		[
14 | 			"@semantic-release/npm",
15 | 			{
16 | 				"npmPublish": true,
17 | 				"pkgRoot": "."
18 | 			}
19 | 		],
20 | 		[
21 | 			"@semantic-release/git",
22 | 			{
23 | 				"assets": [
24 | 					"package.json",
25 | 					"CHANGELOG.md",
26 | 					"src/index.ts",
27 | 					"src/cli/index.ts"
28 | 				],
29 | 				"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
30 | 			}
31 | 		],
32 | 		"@semantic-release/github"
33 | 	]
34 | }
35 | 
```

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

```markdown
  1 | # Connect AI to Your Bitbucket Repositories
  2 | 
  3 | Transform how you work with Bitbucket by connecting Claude, Cursor AI, and other AI assistants directly to your repositories, pull requests, and code. Get instant insights, automate code reviews, and streamline your development workflow.
  4 | 
  5 | [![NPM Version](https://img.shields.io/npm/v/@aashari/mcp-server-atlassian-bitbucket)](https://www.npmjs.com/package/@aashari/mcp-server-atlassian-bitbucket)
  6 | 
  7 | ## What You Can Do
  8 | 
  9 | ✅ **Ask AI about your code**: "What's the latest commit in my main repository?"  
 10 | ✅ **Get PR insights**: "Show me all open pull requests that need review"  
 11 | ✅ **Search your codebase**: "Find all JavaScript files that use the authentication function"  
 12 | ✅ **Review code changes**: "Compare the differences between my feature branch and main"  
 13 | ✅ **Manage pull requests**: "Create a PR for my new-feature branch"  
 14 | ✅ **Automate workflows**: "Add a comment to PR #123 with the test results"  
 15 | 
 16 | ## Perfect For
 17 | 
 18 | - **Developers** who want AI assistance with code reviews and repository management
 19 | - **Team Leads** needing quick insights into project status and pull request activity  
 20 | - **DevOps Engineers** automating repository workflows and branch management
 21 | - **Anyone** who wants to interact with Bitbucket using natural language
 22 | 
 23 | ## Quick Start
 24 | 
 25 | Get up and running in 2 minutes:
 26 | 
 27 | ### 1. Get Your Bitbucket Credentials
 28 | 
 29 | > ⚠️ **IMPORTANT**: Bitbucket App Passwords are being deprecated and will be removed by **June 2026**. We recommend using **Scoped API Tokens** for new setups.
 30 | 
 31 | #### Option A: Scoped API Token (Recommended - Future-Proof)
 32 | 
 33 | **Bitbucket is deprecating app passwords**. Use the new scoped API tokens instead:
 34 | 
 35 | 1. Go to [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
 36 | 2. Click **"Create API token with scopes"**
 37 | 3. Select **"Bitbucket"** as the product
 38 | 4. Choose the appropriate scopes:
 39 |    - **For read-only access**: `repository`, `workspace`
 40 |    - **For full functionality**: `repository`, `workspace`, `pullrequest`
 41 | 5. Copy the generated token (starts with `ATATT`)
 42 | 6. Use with your Atlassian email as the username
 43 | 
 44 | #### Option B: App Password (Legacy - Will be deprecated)
 45 | 
 46 | Generate a Bitbucket App Password (legacy method):
 47 | 1. Go to [Bitbucket App Passwords](https://bitbucket.org/account/settings/app-passwords/)
 48 | 2. Click "Create app password"
 49 | 3. Give it a name like "AI Assistant"
 50 | 4. Select these permissions:
 51 |    - **Workspaces**: Read
 52 |    - **Repositories**: Read (and Write if you want AI to create PRs/comments)
 53 |    - **Pull Requests**: Read (and Write for PR management)
 54 | 
 55 | ### 2. Try It Instantly
 56 | 
 57 | ```bash
 58 | # Set your credentials (choose one method)
 59 | 
 60 | # Method 1: Scoped API Token (recommended - future-proof)
 61 | export ATLASSIAN_USER_EMAIL="[email protected]"
 62 | export ATLASSIAN_API_TOKEN="your_scoped_api_token"  # Token starting with ATATT
 63 | 
 64 | # OR Method 2: Legacy App Password (will be deprecated June 2026)
 65 | export ATLASSIAN_BITBUCKET_USERNAME="your_username"
 66 | export ATLASSIAN_BITBUCKET_APP_PASSWORD="your_app_password"
 67 | 
 68 | # List your workspaces
 69 | npx -y @aashari/mcp-server-atlassian-bitbucket ls-workspaces
 70 | 
 71 | # List repositories in your workspace
 72 | npx -y @aashari/mcp-server-atlassian-bitbucket ls-repos --workspace-slug your-workspace
 73 | 
 74 | # Get details about a specific repository  
 75 | npx -y @aashari/mcp-server-atlassian-bitbucket get-repo --workspace-slug your-workspace --repo-slug your-repo
 76 | ```
 77 | 
 78 | ## Connect to AI Assistants
 79 | 
 80 | ### For Claude Desktop Users
 81 | 
 82 | Add this to your Claude configuration file (`~/.claude/claude_desktop_config.json`):
 83 | 
 84 | **Option 1: Scoped API Token (recommended - future-proof)**
 85 | ```json
 86 | {
 87 |   "mcpServers": {
 88 |     "bitbucket": {
 89 |       "command": "npx",
 90 |       "args": ["-y", "@aashari/mcp-server-atlassian-bitbucket"],
 91 |       "env": {
 92 |         "ATLASSIAN_USER_EMAIL": "[email protected]",
 93 |         "ATLASSIAN_API_TOKEN": "your_scoped_api_token"
 94 |       }
 95 |     }
 96 |   }
 97 | }
 98 | ```
 99 | 
100 | **Option 2: Legacy App Password (will be deprecated June 2026)**
101 | ```json
102 | {
103 |   "mcpServers": {
104 |     "bitbucket": {
105 |       "command": "npx",
106 |       "args": ["-y", "@aashari/mcp-server-atlassian-bitbucket"],
107 |       "env": {
108 |         "ATLASSIAN_BITBUCKET_USERNAME": "your_username",
109 |         "ATLASSIAN_BITBUCKET_APP_PASSWORD": "your_app_password"
110 |       }
111 |     }
112 |   }
113 | }
114 | ```
115 | 
116 | Restart Claude Desktop, and you'll see "🔗 bitbucket" in the status bar.
117 | 
118 | ### For Other AI Assistants
119 | 
120 | Most AI assistants support MCP. Install the server globally:
121 | 
122 | ```bash
123 | npm install -g @aashari/mcp-server-atlassian-bitbucket
124 | ```
125 | 
126 | Then configure your AI assistant to use the MCP server with STDIO transport.
127 | 
128 | ### Alternative: Configuration File
129 | 
130 | Create `~/.mcp/configs.json` for system-wide configuration:
131 | 
132 | **Option 1: Scoped API Token (recommended - future-proof)**
133 | ```json
134 | {
135 |   "bitbucket": {
136 |     "environments": {
137 |       "ATLASSIAN_USER_EMAIL": "[email protected]",
138 |       "ATLASSIAN_API_TOKEN": "your_scoped_api_token",
139 |       "BITBUCKET_DEFAULT_WORKSPACE": "your_main_workspace"
140 |     }
141 |   }
142 | }
143 | ```
144 | 
145 | **Option 2: Legacy App Password (will be deprecated June 2026)**
146 | ```json
147 | {
148 |   "bitbucket": {
149 |     "environments": {
150 |       "ATLASSIAN_BITBUCKET_USERNAME": "your_username",
151 |       "ATLASSIAN_BITBUCKET_APP_PASSWORD": "your_app_password",
152 |       "BITBUCKET_DEFAULT_WORKSPACE": "your_main_workspace"
153 |     }
154 |   }
155 | }
156 | ```
157 | 
158 | **Alternative config keys:** The system also accepts `"atlassian-bitbucket"`, `"@aashari/mcp-server-atlassian-bitbucket"`, or `"mcp-server-atlassian-bitbucket"` instead of `"bitbucket"`.
159 | 
160 | ## Real-World Examples
161 | 
162 | ### 🔍 Explore Your Repositories
163 | 
164 | Ask your AI assistant:
165 | - *"List all repositories in my main workspace"*
166 | - *"Show me details about the backend-api repository"*
167 | - *"What's the commit history for the feature-auth branch?"*
168 | - *"Get the content of src/config.js from the main branch"*
169 | 
170 | ### 📋 Manage Pull Requests
171 | 
172 | Ask your AI assistant:
173 | - *"Show me all open pull requests that need review"*
174 | - *"Get details about pull request #42 including the code changes"*
175 | - *"Create a pull request from feature-login to main branch"*
176 | - *"Add a comment to PR #15 saying the tests passed"*
177 | - *"Approve pull request #33"*
178 | 
179 | ### 🔧 Work with Branches and Code
180 | 
181 | Ask your AI assistant:
182 | - *"Compare my feature branch with the main branch"*
183 | - *"Create a new branch called hotfix-login from the main branch"*
184 | - *"List all branches in the user-service repository"*
185 | - *"Show me the differences between commits abc123 and def456"*
186 | 
187 | ### 🔎 Search and Discovery
188 | 
189 | Ask your AI assistant:
190 | - *"Search for JavaScript files that contain 'authentication'"*
191 | - *"Find all pull requests related to the login feature"*
192 | - *"Search for repositories in the mobile project"*
193 | - *"Show me code files that use the React framework"*
194 | 
195 | ## Troubleshooting
196 | 
197 | ### "Authentication failed" or "403 Forbidden"
198 | 
199 | 1. **Choose the right authentication method**:
200 |    - **Standard Atlassian method**: Use your Atlassian account email + API token (works with any Atlassian service)
201 |    - **Bitbucket-specific method**: Use your Bitbucket username + App password (Bitbucket only)
202 | 
203 | 2. **For Bitbucket App Passwords** (if using Option 2):
204 |    - Go to [Bitbucket App Passwords](https://bitbucket.org/account/settings/app-passwords/)
205 |    - Make sure your app password has the right permissions (Workspaces: Read, Repositories: Read, Pull Requests: Read)
206 | 
207 | 3. **For Scoped API Tokens** (recommended):
208 |    - Go to [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
209 |    - Make sure your token is still active and has the right scopes
210 |    - Update your `~/.mcp/configs.json` file to use the new scoped API token format:
211 |    ```json
212 |    {
213 |      "@aashari/mcp-server-atlassian-bitbucket": {
214 |        "environments": {
215 |          "ATLASSIAN_USER_EMAIL": "[email protected]",
216 |          "ATLASSIAN_API_TOKEN": "ATATT3xFfGF0..."
217 |        }
218 |      }
219 |    }
220 |    ```
221 | 
222 | 4. **Verify your credentials**:
223 |    ```bash
224 |    # Test your credentials work
225 |    npx -y @aashari/mcp-server-atlassian-bitbucket ls-workspaces
226 |    ```
227 | 
228 | ### "Workspace not found" or "Repository not found"
229 | 
230 | 1. **Check your workspace slug**:
231 |    ```bash
232 |    # List your workspaces to see the correct slugs
233 |    npx -y @aashari/mcp-server-atlassian-bitbucket ls-workspaces
234 |    ```
235 | 
236 | 2. **Use the exact slug from Bitbucket URL**:
237 |    - If your repo URL is `https://bitbucket.org/myteam/my-repo`
238 |    - Workspace slug is `myteam`
239 |    - Repository slug is `my-repo`
240 | 
241 | ### "No default workspace configured"
242 | 
243 | Set a default workspace to avoid specifying it every time:
244 | ```bash
245 | export BITBUCKET_DEFAULT_WORKSPACE="your-main-workspace-slug"
246 | ```
247 | 
248 | ### Claude Desktop Integration Issues
249 | 
250 | 1. **Restart Claude Desktop** after updating the config file
251 | 2. **Check the status bar** for the "🔗 bitbucket" indicator
252 | 3. **Verify config file location**:
253 |    - macOS: `~/.claude/claude_desktop_config.json`
254 |    - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
255 | 
256 | ### Getting Help
257 | 
258 | If you're still having issues:
259 | 1. Run a simple test command to verify everything works
260 | 2. Check the [GitHub Issues](https://github.com/aashari/mcp-server-atlassian-bitbucket/issues) for similar problems
261 | 3. Create a new issue with your error message and setup details
262 | 
263 | ## Frequently Asked Questions
264 | 
265 | ### What permissions do I need?
266 | 
267 | **For Scoped API Tokens** (recommended):
268 | - Your regular Atlassian account with access to Bitbucket
269 | - Scoped API token created at [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
270 | - Required scopes: `repository`, `workspace` (add `pullrequest` for PR management)
271 | 
272 | **For Bitbucket App Passwords** (legacy - being deprecated):
273 | - For **read-only access** (viewing repos, PRs, commits):
274 |   - Workspaces: Read
275 |   - Repositories: Read  
276 |   - Pull Requests: Read
277 | - For **full functionality** (creating PRs, commenting):
278 |   - Add "Write" permissions for Repositories and Pull Requests
279 | 
280 | ### Can I use this with private repositories?
281 | 
282 | Yes! This works with both public and private repositories. You just need the appropriate permissions through your Bitbucket App Password.
283 | 
284 | ### Do I need to specify workspace every time?
285 | 
286 | No! Set `BITBUCKET_DEFAULT_WORKSPACE` in your environment or config file, and it will be used automatically when you don't specify one.
287 | 
288 | ### What AI assistants does this work with?
289 | 
290 | Any AI assistant that supports the Model Context Protocol (MCP):
291 | - Claude Desktop (most popular)
292 | - Cursor AI
293 | - Continue.dev
294 | - Many others
295 | 
296 | ### Is my data secure?
297 | 
298 | Yes! This tool:
299 | - Runs entirely on your local machine
300 | - Uses your own Bitbucket credentials
301 | - Never sends your data to third parties
302 | - Only accesses what you give it permission to access
303 | 
304 | ### Can I use this for multiple Bitbucket accounts?
305 | 
306 | Currently, each installation supports one set of credentials. For multiple accounts, you'd need separate configurations.
307 | 
308 | ## Support
309 | 
310 | Need help? Here's how to get assistance:
311 | 
312 | 1. **Check the troubleshooting section above** - most common issues are covered there
313 | 2. **Visit our GitHub repository** for documentation and examples: [github.com/aashari/mcp-server-atlassian-bitbucket](https://github.com/aashari/mcp-server-atlassian-bitbucket)
314 | 3. **Report issues** at [GitHub Issues](https://github.com/aashari/mcp-server-atlassian-bitbucket/issues)
315 | 4. **Start a discussion** for feature requests or general questions
316 | 
317 | ---
318 | 
319 | *Made with ❤️ for developers who want to bring AI into their Bitbucket workflow.*
320 | 
```

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

```json
1 | {
2 |   "type": "module"
3 | } 
```

--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------

```yaml
 1 | version: 2
 2 | updates:
 3 |   - package-ecosystem: "npm"
 4 |     directory: "/"
 5 |     schedule:
 6 |       interval: "weekly"
 7 |     open-pull-requests-limit: 10
 8 |     versioning-strategy: auto
 9 |     labels:
10 |       - "dependencies"
11 |     commit-message:
12 |       prefix: "chore"
13 |       include: "scope"
14 |     allow:
15 |       - dependency-type: "direct"
16 |     ignore:
17 |       - dependency-name: "*"
18 |         update-types: ["version-update:semver-patch"]
19 |   - package-ecosystem: "github-actions"
20 |     directory: "/"
21 |     schedule:
22 |       interval: "weekly"
23 |     open-pull-requests-limit: 5
24 |     labels:
25 |       - "dependencies"
26 |       - "github-actions" 
```

--------------------------------------------------------------------------------
/src/utils/atlassian.util.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Types of content that can be searched in Bitbucket
 3 |  */
 4 | export enum ContentType {
 5 | 	WIKI = 'wiki',
 6 | 	ISSUE = 'issue',
 7 | 	PULLREQUEST = 'pullrequest',
 8 | 	COMMIT = 'commit',
 9 | 	BRANCH = 'branch',
10 | 	TAG = 'tag',
11 | }
12 | 
13 | /**
14 |  * Get the display name for a content type
15 |  */
16 | export function getContentTypeDisplay(type: ContentType): string {
17 | 	switch (type) {
18 | 		case ContentType.WIKI:
19 | 			return 'Wiki';
20 | 		case ContentType.ISSUE:
21 | 			return 'Issue';
22 | 		case ContentType.PULLREQUEST:
23 | 			return 'Pull Request';
24 | 		case ContentType.COMMIT:
25 | 			return 'Commit';
26 | 		case ContentType.BRANCH:
27 | 			return 'Branch';
28 | 		case ContentType.TAG:
29 | 			return 'Tag';
30 | 		default:
31 | 			return type;
32 | 	}
33 | }
34 | 
```

--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------

```javascript
 1 | // Jest setup file to suppress console warnings during tests
 2 | // This improves test output readability while maintaining error visibility
 3 | 
 4 | const originalConsoleWarn = console.warn;
 5 | const originalConsoleInfo = console.info;
 6 | const originalConsoleDebug = console.debug;
 7 | 
 8 | beforeAll(() => {
 9 |   // Suppress console.warn, console.info, and console.debug during tests
10 |   // while keeping console.error for actual issues
11 |   console.warn = jest.fn();
12 |   console.info = jest.fn();
13 |   console.debug = jest.fn();
14 | });
15 | 
16 | afterAll(() => {
17 |   // Restore original console methods
18 |   console.warn = originalConsoleWarn;
19 |   console.info = originalConsoleInfo;
20 |   console.debug = originalConsoleDebug;
21 | });
```

--------------------------------------------------------------------------------
/.github/workflows/ci-dependency-check.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI - Dependency Check
 2 | 
 3 | on:
 4 |     schedule:
 5 |         - cron: '0 5 * * 1' # Run at 5 AM UTC every Monday
 6 |     workflow_dispatch: # Allow manual triggering
 7 | 
 8 | jobs:
 9 |     validate:
10 |         runs-on: ubuntu-latest
11 |         steps:
12 |             - name: Checkout code
13 |               uses: actions/checkout@v5
14 | 
15 |             - name: Setup Node.js
16 |               uses: actions/setup-node@v5
17 |               with:
18 |                   node-version: '22'
19 |                   cache: 'npm'
20 | 
21 |             - name: Install dependencies
22 |               run: npm ci
23 | 
24 |             - name: Run npm audit
25 |               run: npm audit
26 | 
27 |             - name: Check for outdated dependencies
28 |               run: npm outdated || true
29 | 
30 |             - name: Run tests
31 |               run: npm test
32 | 
33 |             - name: Run linting
34 |               run: npm run lint
35 | 
36 |             - name: Build project
37 |               run: npm run build
38 | 
```

--------------------------------------------------------------------------------
/src/utils/defaults.util.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Default values for pagination across the application.
 3 |  * These values should be used consistently throughout the codebase.
 4 |  */
 5 | 
 6 | /**
 7 |  * Default page size for all list operations.
 8 |  * This value determines how many items are returned in a single page by default.
 9 |  */
10 | export const DEFAULT_PAGE_SIZE = 25;
11 | 
12 | /**
13 |  * Apply default values to options object.
14 |  * This utility ensures that default values are consistently applied.
15 |  *
16 |  * @param options Options object that may have some values undefined
17 |  * @param defaults Default values to apply when options values are undefined
18 |  * @returns Options object with default values applied
19 |  *
20 |  * @example
21 |  * const options = applyDefaults({ limit: 10 }, { limit: DEFAULT_PAGE_SIZE, includeBranches: true });
22 |  * // Result: { limit: 10, includeBranches: true }
23 |  */
24 | export function applyDefaults<T extends object>(
25 | 	options: Partial<T>,
26 | 	defaults: Partial<T>,
27 | ): T {
28 | 	return {
29 | 		...defaults,
30 | 		...Object.fromEntries(
31 | 			Object.entries(options).filter(([_, value]) => value !== undefined),
32 | 		),
33 | 	} as T;
34 | }
35 | 
```

--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------

```
 1 | import eslint from '@eslint/js';
 2 | import tseslint from 'typescript-eslint';
 3 | import prettierPlugin from 'eslint-plugin-prettier';
 4 | import eslintConfigPrettier from 'eslint-config-prettier';
 5 | 
 6 | export default tseslint.config(
 7 | 	{
 8 | 		ignores: ['node_modules/**', 'dist/**', 'examples/**'],
 9 | 	},
10 | 	eslint.configs.recommended,
11 | 	...tseslint.configs.recommended,
12 | 	{
13 | 		plugins: {
14 | 			prettier: prettierPlugin,
15 | 		},
16 | 		rules: {
17 | 			'prettier/prettier': 'error',
18 | 			indent: ['error', 'tab', { SwitchCase: 1 }],
19 | 			'@typescript-eslint/no-unused-vars': [
20 | 				'error',
21 | 				{ argsIgnorePattern: '^_' },
22 | 			],
23 | 		},
24 | 		languageOptions: {
25 | 			parserOptions: {
26 | 				ecmaVersion: 'latest',
27 | 				sourceType: 'module',
28 | 			},
29 | 			globals: {
30 | 				node: 'readonly',
31 | 				jest: 'readonly',
32 | 			},
33 | 		},
34 | 	},
35 | 	// Special rules for test files
36 | 	{
37 | 		files: ['**/*.test.ts'],
38 | 		rules: {
39 | 			'@typescript-eslint/no-explicit-any': 'off',
40 | 			'@typescript-eslint/no-require-imports': 'off',
41 | 			'@typescript-eslint/no-unsafe-function-type': 'off',
42 | 			'@typescript-eslint/no-unused-vars': 'off',
43 | 		},
44 | 	},
45 | 	eslintConfigPrettier,
46 | );
47 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci-dependabot-auto-merge.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI - Dependabot Auto-merge
 2 | 
 3 | on:
 4 |     pull_request:
 5 |         branches: [main]
 6 | 
 7 | permissions:
 8 |     contents: write
 9 |     pull-requests: write
10 |     checks: read
11 | 
12 | jobs:
13 |     auto-merge-dependabot:
14 |         runs-on: ubuntu-latest
15 |         if: github.actor == 'dependabot[bot]'
16 |         steps:
17 |             - name: Checkout code
18 |               uses: actions/checkout@v5
19 | 
20 |             - name: Setup Node.js
21 |               uses: actions/setup-node@v5
22 |               with:
23 |                   node-version: '22'
24 |                   cache: 'npm'
25 | 
26 |             - name: Install dependencies
27 |               run: npm ci
28 | 
29 |             - name: Run tests
30 |               run: npm test
31 | 
32 |             - name: Run linting
33 |               run: npm run lint
34 | 
35 |             - name: Auto-approve PR
36 |               uses: hmarr/auto-approve-action@v4
37 |               with:
38 |                   github-token: ${{ secrets.GITHUB_TOKEN }}
39 | 
40 |             - name: Enable auto-merge
41 |               if: success()
42 |               run: gh pr merge --auto --merge "$PR_URL"
43 |               env:
44 |                   PR_URL: ${{ github.event.pull_request.html_url }}
45 |                   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 | 
```

--------------------------------------------------------------------------------
/src/tools/atlassian.workspaces.types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | /**
 4 |  * Base pagination arguments for all tools
 5 |  */
 6 | const PaginationArgs = {
 7 | 	limit: z
 8 | 		.number()
 9 | 		.int()
10 | 		.positive()
11 | 		.max(100)
12 | 		.optional()
13 | 		.describe(
14 | 			'Maximum number of items to return (1-100). Controls the response size. Defaults to 25 if omitted.',
15 | 		),
16 | 
17 | 	cursor: z
18 | 		.string()
19 | 		.optional()
20 | 		.describe(
21 | 			'Pagination cursor for retrieving the next set of results. Obtained from previous response when more results are available.',
22 | 		),
23 | };
24 | 
25 | /**
26 |  * Schema for list-workspaces tool arguments
27 |  */
28 | export const ListWorkspacesToolArgs = z.object({
29 | 	/**
30 | 	 * Maximum number of workspaces to return and pagination
31 | 	 */
32 | 	...PaginationArgs,
33 | });
34 | 
35 | export type ListWorkspacesToolArgsType = z.infer<typeof ListWorkspacesToolArgs>;
36 | 
37 | /**
38 |  * Schema for get-workspace tool arguments
39 |  */
40 | export const GetWorkspaceToolArgs = z.object({
41 | 	/**
42 | 	 * Workspace slug to retrieve
43 | 	 */
44 | 	workspaceSlug: z
45 | 		.string()
46 | 		.min(1, 'Workspace slug is required')
47 | 		.describe(
48 | 			'Workspace slug to retrieve detailed information for. Must be a valid workspace slug from your Bitbucket account. Example: "myteam"',
49 | 		),
50 | });
51 | 
52 | export type GetWorkspaceToolArgsType = z.infer<typeof GetWorkspaceToolArgs>;
53 | 
```

--------------------------------------------------------------------------------
/src/services/vendor.atlassian.search.types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ContentType } from '../utils/atlassian.util.js';
 2 | 
 3 | /**
 4 |  * Content search parameters
 5 |  */
 6 | export interface ContentSearchParams {
 7 | 	/** Workspace slug to search in */
 8 | 	workspaceSlug: string;
 9 | 	/** Query string to search for */
10 | 	query: string;
11 | 	/** Maximum number of results to return (default: 25) */
12 | 	limit?: number;
13 | 	/** Page number for pagination (default: 1) */
14 | 	page?: number;
15 | 	/** Repository slug to search in (optional) */
16 | 	repoSlug?: string;
17 | 	/** Type of content to search for (optional) */
18 | 	contentType?: ContentType;
19 | }
20 | 
21 | /**
22 |  * Generic content search result item
23 |  */
24 | export interface ContentSearchResultItem {
25 | 	// Most Bitbucket content items will have these fields
26 | 	type?: string;
27 | 	title?: string;
28 | 	name?: string;
29 | 	summary?: string;
30 | 	description?: string;
31 | 	content?: string;
32 | 	created_on?: string;
33 | 	updated_on?: string;
34 | 	links?: {
35 | 		self?: { href: string };
36 | 		html?: { href: string };
37 | 		[key: string]: unknown;
38 | 	};
39 | 	// Allow additional properties as Bitbucket returns different fields per content type
40 | 	[key: string]: unknown;
41 | }
42 | 
43 | /**
44 |  * Content search response
45 |  */
46 | export interface ContentSearchResponse {
47 | 	size: number;
48 | 	page: number;
49 | 	pagelen: number;
50 | 	values: ContentSearchResultItem[];
51 | 	next?: string;
52 | 	previous?: string;
53 | }
54 | 
```

--------------------------------------------------------------------------------
/src/utils/constants.util.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Application constants
 3 |  *
 4 |  * This file contains constants used throughout the application.
 5 |  * Centralizing these values makes them easier to maintain and update.
 6 |  */
 7 | 
 8 | /**
 9 |  * Current application version
10 |  * This should match the version in package.json
11 |  */
12 | export const VERSION = '1.23.6';
13 | 
14 | /**
15 |  * Package name with scope
16 |  * Used for initialization and identification
17 |  */
18 | export const PACKAGE_NAME = '@aashari/mcp-server-atlassian-bitbucket';
19 | 
20 | /**
21 |  * CLI command name
22 |  * Used for binary name and CLI help text
23 |  */
24 | export const CLI_NAME = 'mcp-atlassian-bitbucket';
25 | 
26 | /**
27 |  * Network timeout constants (in milliseconds)
28 |  */
29 | export const NETWORK_TIMEOUTS = {
30 | 	/** Default timeout for API requests (30 seconds) */
31 | 	DEFAULT_REQUEST_TIMEOUT: 30 * 1000,
32 | 
33 | 	/** Timeout for large file operations like diffs (60 seconds) */
34 | 	LARGE_REQUEST_TIMEOUT: 60 * 1000,
35 | 
36 | 	/** Timeout for search operations (45 seconds) */
37 | 	SEARCH_REQUEST_TIMEOUT: 45 * 1000,
38 | } as const;
39 | 
40 | /**
41 |  * Data limits to prevent excessive resource consumption (CWE-770)
42 |  */
43 | export const DATA_LIMITS = {
44 | 	/** Maximum response size in bytes (10MB) */
45 | 	MAX_RESPONSE_SIZE: 10 * 1024 * 1024,
46 | 
47 | 	/** Maximum items per page for paginated requests */
48 | 	MAX_PAGE_SIZE: 100,
49 | 
50 | 	/** Default page size when not specified */
51 | 	DEFAULT_PAGE_SIZE: 50,
52 | } as const;
53 | 
```

--------------------------------------------------------------------------------
/scripts/ensure-executable.js:
--------------------------------------------------------------------------------

```javascript
 1 | #!/usr/bin/env node
 2 | import fs from 'fs';
 3 | import path from 'path';
 4 | import { fileURLToPath } from 'url';
 5 | 
 6 | // Use dynamic import meta for ESM compatibility
 7 | const __filename = fileURLToPath(import.meta.url);
 8 | const __dirname = path.dirname(__filename);
 9 | const rootDir = path.resolve(__dirname, '..');
10 | const entryPoint = path.join(rootDir, 'dist', 'index.js');
11 | 
12 | try {
13 | 	if (fs.existsSync(entryPoint)) {
14 | 		// Ensure the file is executable (cross-platform)
15 | 		const currentMode = fs.statSync(entryPoint).mode;
16 | 		// Check if executable bits are set (user, group, or other)
17 | 		// Mode constants differ slightly across platforms, checking broadly
18 | 		const isExecutable =
19 | 			currentMode & fs.constants.S_IXUSR ||
20 | 			currentMode & fs.constants.S_IXGRP ||
21 | 			currentMode & fs.constants.S_IXOTH;
22 | 
23 | 		if (!isExecutable) {
24 | 			// Set permissions to 755 (rwxr-xr-x) if not executable
25 | 			fs.chmodSync(entryPoint, 0o755);
26 | 			console.log(
27 | 				`Made ${path.relative(rootDir, entryPoint)} executable`,
28 | 			);
29 | 		} else {
30 | 			// console.log(`${path.relative(rootDir, entryPoint)} is already executable`);
31 | 		}
32 | 	} else {
33 | 		// console.warn(`${path.relative(rootDir, entryPoint)} not found, skipping chmod`);
34 | 	}
35 | } catch (err) {
36 | 	// console.warn(`Failed to set executable permissions: ${err.message}`);
37 | 	// We use '|| true' in package.json, so no need to exit here
38 | }
39 | 
```

--------------------------------------------------------------------------------
/src/services/vendor.atlassian.repositories.diff.types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | /**
 4 |  * Parameters for retrieving diffstat between two refs (branches, tags, or commit hashes)
 5 |  */
 6 | export const GetDiffstatParamsSchema = z.object({
 7 | 	workspace: z.string().min(1, 'Workspace is required'),
 8 | 	repo_slug: z.string().min(1, 'Repository slug is required'),
 9 | 	/** e.g., "main..feature" or "hashA..hashB" */
10 | 	spec: z.string().min(1, 'Diff spec is required'),
11 | 	pagelen: z.number().int().positive().optional(),
12 | 	cursor: z.number().int().positive().optional(), // Bitbucket page-based cursor
13 | 	topic: z.boolean().optional(),
14 | });
15 | 
16 | export type GetDiffstatParams = z.infer<typeof GetDiffstatParamsSchema>;
17 | 
18 | export const GetRawDiffParamsSchema = z.object({
19 | 	workspace: z.string().min(1),
20 | 	repo_slug: z.string().min(1),
21 | 	spec: z.string().min(1),
22 | });
23 | 
24 | export type GetRawDiffParams = z.infer<typeof GetRawDiffParamsSchema>;
25 | 
26 | /**
27 |  * Schema for a single file change entry in diffstat
28 |  */
29 | export const DiffstatFileChangeSchema = z.object({
30 | 	status: z.string(),
31 | 	old: z
32 | 		.object({
33 | 			path: z.string(),
34 | 			type: z.string().optional(),
35 | 		})
36 | 		.nullable()
37 | 		.optional(),
38 | 	new: z
39 | 		.object({
40 | 			path: z.string(),
41 | 			type: z.string().optional(),
42 | 		})
43 | 		.nullable()
44 | 		.optional(),
45 | 	lines_added: z.number().optional(),
46 | 	lines_removed: z.number().optional(),
47 | });
48 | 
49 | /**
50 |  * Schema for diffstat API response (paginated)
51 |  */
52 | export const DiffstatResponseSchema = z.object({
53 | 	pagelen: z.number().optional(),
54 | 	values: z.array(DiffstatFileChangeSchema),
55 | 	page: z.number().optional(),
56 | 	size: z.number().optional(),
57 | 	next: z.string().optional(),
58 | 	previous: z.string().optional(),
59 | });
60 | 
61 | export type DiffstatResponse = z.infer<typeof DiffstatResponseSchema>;
62 | 
```

--------------------------------------------------------------------------------
/src/utils/query.util.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Utilities for formatting and handling query parameters for the Bitbucket API.
 3 |  * These functions help convert user-friendly query strings into the format expected by Bitbucket's REST API.
 4 |  */
 5 | 
 6 | /**
 7 |  * Format a simple text query into Bitbucket's query syntax
 8 |  * Bitbucket API expects query parameters in a specific format for the 'q' parameter
 9 |  *
10 |  * @param query - The search query string
11 |  * @param field - Optional field to search in, defaults to 'name'
12 |  * @returns Formatted query string for Bitbucket API
13 |  *
14 |  * @example
15 |  * // Simple text search (returns: name ~ "vue3")
16 |  * formatBitbucketQuery("vue3")
17 |  *
18 |  * @example
19 |  * // Already formatted query (returns unchanged: name = "repository")
20 |  * formatBitbucketQuery("name = \"repository\"")
21 |  *
22 |  * @example
23 |  * // With specific field (returns: description ~ "API")
24 |  * formatBitbucketQuery("API", "description")
25 |  */
26 | export function formatBitbucketQuery(
27 | 	query: string,
28 | 	field: string = 'name',
29 | ): string {
30 | 	// If the query is empty, return it as is
31 | 	if (!query || query.trim() === '') {
32 | 		return query;
33 | 	}
34 | 
35 | 	// Regular expression to check if the query already contains operators
36 | 	// like ~, =, !=, >, <, etc., which would indicate it's already formatted
37 | 	const operatorPattern = /[~=!<>]/;
38 | 
39 | 	// If the query already contains operators, assume it's properly formatted
40 | 	if (operatorPattern.test(query)) {
41 | 		return query;
42 | 	}
43 | 
44 | 	// If query is quoted, assume it's an exact match
45 | 	if (query.startsWith('"') && query.endsWith('"')) {
46 | 		return `${field} ~ ${query}`;
47 | 	}
48 | 
49 | 	// Format simple text as a field search with fuzzy match
50 | 	// Wrap in double quotes to handle spaces and special characters
51 | 	return `${field} ~ "${query}"`;
52 | }
53 | 
```

--------------------------------------------------------------------------------
/src/types/common.types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Common type definitions shared across controllers.
 3 |  * These types provide a standard interface for controller interactions.
 4 |  * Centralized here to ensure consistency across the codebase.
 5 |  */
 6 | 
 7 | /**
 8 |  * Common pagination information for API responses.
 9 |  * This is used for providing consistent pagination details internally.
10 |  * Its formatted representation will be included directly in the content string.
11 |  */
12 | export interface ResponsePagination {
13 | 	/**
14 | 	 * Cursor for the next page of results, if available.
15 | 	 * This should be passed to subsequent requests to retrieve the next page.
16 | 	 */
17 | 	nextCursor?: string;
18 | 
19 | 	/**
20 | 	 * Whether more results are available beyond the current page.
21 | 	 * When true, clients should use the nextCursor to retrieve more results.
22 | 	 */
23 | 	hasMore: boolean;
24 | 
25 | 	/**
26 | 	 * The number of items in the current result set.
27 | 	 * This helps clients track how many items they've received.
28 | 	 */
29 | 	count?: number;
30 | 
31 | 	/**
32 | 	 * The total number of items available across all pages, if known.
33 | 	 * Note: Not all APIs provide this. Check the specific API/tool documentation.
34 | 	 */
35 | 	total?: number;
36 | 
37 | 	/**
38 | 	 * Page number for page-based pagination.
39 | 	 */
40 | 	page?: number;
41 | 
42 | 	/**
43 | 	 * Page size for page-based pagination.
44 | 	 */
45 | 	size?: number;
46 | }
47 | 
48 | /**
49 |  * Common response structure for controller operations.
50 |  * All controller methods should return this structure.
51 |  */
52 | export interface ControllerResponse {
53 | 	/**
54 | 	 * Formatted content to be displayed to the user.
55 | 	 * Contains a comprehensive Markdown-formatted string that includes all information:
56 | 	 * - Primary content (e.g., list items, details)
57 | 	 * - Any metadata (previously in metadata field)
58 | 	 * - Pagination information (previously in pagination field)
59 | 	 */
60 | 	content: string;
61 | }
62 | 
```

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

```typescript
 1 | import { Command } from 'commander';
 2 | import { Logger } from '../utils/logger.util.js';
 3 | import { VERSION, CLI_NAME } from '../utils/constants.util.js';
 4 | 
 5 | // Import Bitbucket-specific CLI modules
 6 | import atlassianWorkspacesCli from './atlassian.workspaces.cli.js';
 7 | import atlassianRepositoriesCli from './atlassian.repositories.cli.js';
 8 | import atlassianPullRequestsCli from './atlassian.pullrequests.cli.js';
 9 | import atlassianSearchCommands from './atlassian.search.cli.js';
10 | import diffCli from './atlassian.diff.cli.js';
11 | 
12 | // Package description
13 | const DESCRIPTION =
14 | 	'A Model Context Protocol (MCP) server for Atlassian Bitbucket integration';
15 | 
16 | // Create a contextualized logger for this file
17 | const cliLogger = Logger.forContext('cli/index.ts');
18 | 
19 | // Log CLI initialization
20 | cliLogger.debug('Bitbucket CLI module initialized');
21 | 
22 | export async function runCli(args: string[]) {
23 | 	const methodLogger = Logger.forContext('cli/index.ts', 'runCli');
24 | 
25 | 	const program = new Command();
26 | 
27 | 	program.name(CLI_NAME).description(DESCRIPTION).version(VERSION);
28 | 
29 | 	// Register CLI commands
30 | 	atlassianWorkspacesCli.register(program);
31 | 	cliLogger.debug('Workspace commands registered');
32 | 
33 | 	atlassianRepositoriesCli.register(program);
34 | 	cliLogger.debug('Repository commands registered');
35 | 
36 | 	atlassianPullRequestsCli.register(program);
37 | 	cliLogger.debug('Pull Request commands registered');
38 | 
39 | 	atlassianSearchCommands.register(program);
40 | 	cliLogger.debug('Search commands registered');
41 | 
42 | 	diffCli.register(program);
43 | 	cliLogger.debug('Diff commands registered');
44 | 
45 | 	// Handle unknown commands
46 | 	program.on('command:*', (operands) => {
47 | 		methodLogger.error(`Unknown command: ${operands[0]}`);
48 | 		console.log('');
49 | 		program.help();
50 | 		process.exit(1);
51 | 	});
52 | 
53 | 	// Parse arguments; default to help if no command provided
54 | 	await program.parseAsync(args.length ? args : ['--help'], { from: 'user' });
55 | }
56 | 
```

--------------------------------------------------------------------------------
/src/utils/shell.util.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { promisify } from 'util';
 2 | import { exec as callbackExec } from 'child_process';
 3 | import { Logger } from './logger.util.js';
 4 | 
 5 | const exec = promisify(callbackExec);
 6 | const utilLogger = Logger.forContext('utils/shell.util.ts');
 7 | 
 8 | /**
 9 |  * Executes a shell command.
10 |  *
11 |  * @param command The command string to execute.
12 |  * @param operationDesc A brief description of the operation for logging purposes.
13 |  * @returns A promise that resolves with the stdout of the command.
14 |  * @throws An error if the command execution fails, including stderr.
15 |  */
16 | export async function executeShellCommand(
17 | 	command: string,
18 | 	operationDesc: string,
19 | ): Promise<string> {
20 | 	const methodLogger = utilLogger.forMethod('executeShellCommand');
21 | 	methodLogger.debug(`Attempting to ${operationDesc}: ${command}`);
22 | 	try {
23 | 		const { stdout, stderr } = await exec(command);
24 | 		if (stderr) {
25 | 			methodLogger.warn(`Stderr from ${operationDesc}: ${stderr}`);
26 | 			// Depending on the command, stderr might not always indicate a failure,
27 | 			// but for git clone, it usually does if stdout is empty.
28 | 			// If stdout is also present, it might be a warning.
29 | 		}
30 | 		methodLogger.info(
31 | 			`Successfully executed ${operationDesc}. Stdout: ${stdout}`,
32 | 		);
33 | 		return stdout || `Successfully ${operationDesc}.`; // Return stdout or a generic success message
34 | 	} catch (error: unknown) {
35 | 		methodLogger.error(`Failed to ${operationDesc}: ${command}`, error);
36 | 
37 | 		let errorMessage = 'Unknown error during shell command execution.';
38 | 		if (error instanceof Error) {
39 | 			// Node's child_process.ExecException often has stdout and stderr properties
40 | 			const execError = error as Error & {
41 | 				stdout?: string;
42 | 				stderr?: string;
43 | 			};
44 | 			errorMessage =
45 | 				execError.stderr || execError.stdout || execError.message;
46 | 		} else if (typeof error === 'string') {
47 | 			errorMessage = error;
48 | 		}
49 | 		// Ensure a default message if somehow it's still undefined (though unlikely with above checks)
50 | 		if (!errorMessage && error) {
51 | 			errorMessage = String(error);
52 | 		}
53 | 
54 | 		throw new Error(`Failed to ${operationDesc}: ${errorMessage}`);
55 | 	}
56 | }
57 | 
```

--------------------------------------------------------------------------------
/src/utils/path.util.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import * as path from 'path';
 2 | import { Logger } from './logger.util.js';
 3 | 
 4 | const logger = Logger.forContext('utils/path.util.ts');
 5 | 
 6 | /**
 7 |  * Safely converts a path or path segments to a string, handling various input types.
 8 |  * Useful for ensuring consistent path string representations across different platforms.
 9 |  *
10 |  * @param pathInput - The path or path segments to convert to a string
11 |  * @returns The path as a normalized string
12 |  */
13 | export function pathToString(pathInput: string | string[] | unknown): string {
14 | 	if (Array.isArray(pathInput)) {
15 | 		return path.join(...pathInput);
16 | 	} else if (typeof pathInput === 'string') {
17 | 		return pathInput;
18 | 	} else if (pathInput instanceof URL) {
19 | 		return pathInput.pathname;
20 | 	} else if (
21 | 		pathInput &&
22 | 		typeof pathInput === 'object' &&
23 | 		'toString' in pathInput
24 | 	) {
25 | 		return String(pathInput);
26 | 	}
27 | 
28 | 	logger.warn(`Unable to convert path input to string: ${typeof pathInput}`);
29 | 	return ''; // Return empty string for null/undefined
30 | }
31 | 
32 | /**
33 |  * Determines if a given path is within the user's home directory
34 |  * which is generally considered a safe location for MCP operations.
35 |  *
36 |  * @param inputPath - Path to check
37 |  * @returns True if the path is within the user's home directory
38 |  */
39 | export function isPathInHome(inputPath: string): boolean {
40 | 	const homePath = process.env.HOME || process.env.USERPROFILE || '';
41 | 	if (!homePath) {
42 | 		logger.warn('Could not determine user home directory');
43 | 		return false;
44 | 	}
45 | 
46 | 	const resolvedPath = path.resolve(inputPath);
47 | 	return resolvedPath.startsWith(homePath);
48 | }
49 | 
50 | /**
51 |  * Gets a user-friendly display version of a path for use in messages.
52 |  * For paths within the home directory, can replace with ~ for brevity.
53 |  *
54 |  * @param inputPath - Path to format
55 |  * @param useHomeTilde - Whether to replace home directory with ~ symbol
56 |  * @returns Formatted path string
57 |  */
58 | export function formatDisplayPath(
59 | 	inputPath: string,
60 | 	useHomeTilde = true,
61 | ): string {
62 | 	const homePath = process.env.HOME || process.env.USERPROFILE || '';
63 | 
64 | 	if (
65 | 		useHomeTilde &&
66 | 		homePath &&
67 | 		path.resolve(inputPath).startsWith(homePath)
68 | 	) {
69 | 		return path.resolve(inputPath).replace(homePath, '~');
70 | 	}
71 | 
72 | 	return path.resolve(inputPath);
73 | }
74 | 
```

--------------------------------------------------------------------------------
/src/utils/markdown.util.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { htmlToMarkdown } from './markdown.util.js';
 2 | 
 3 | describe('Markdown Utility', () => {
 4 | 	describe('htmlToMarkdown', () => {
 5 | 		it('should convert basic HTML to Markdown', () => {
 6 | 			const html =
 7 | 				'<h1>Hello World</h1><p>This is a <strong>test</strong>.</p>';
 8 | 			const expected = '# Hello World\n\nThis is a **test**.';
 9 | 			expect(htmlToMarkdown(html)).toBe(expected);
10 | 		});
11 | 
12 | 		it('should handle empty input', () => {
13 | 			expect(htmlToMarkdown('')).toBe('');
14 | 			expect(htmlToMarkdown('   ')).toBe('');
15 | 		});
16 | 
17 | 		it('should convert links correctly', () => {
18 | 			const html =
19 | 				'<p>Check out <a href="https://example.com">this link</a>.</p>';
20 | 			const expected = 'Check out [this link](https://example.com).';
21 | 			expect(htmlToMarkdown(html)).toBe(expected);
22 | 		});
23 | 
24 | 		it('should convert lists correctly', () => {
25 | 			const html =
26 | 				'<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>';
27 | 			const expected = '-   Item 1\n-   Item 2\n-   Item 3';
28 | 			expect(htmlToMarkdown(html)).toBe(expected);
29 | 		});
30 | 
31 | 		it('should convert tables correctly', () => {
32 | 			const html = `
33 |                 <table>
34 |                     <thead>
35 |                         <tr>
36 |                             <th>Header 1</th>
37 |                             <th>Header 2</th>
38 |                         </tr>
39 |                     </thead>
40 |                     <tbody>
41 |                         <tr>
42 |                             <td>Cell 1</td>
43 |                             <td>Cell 2</td>
44 |                         </tr>
45 |                         <tr>
46 |                             <td>Cell 3</td>
47 |                             <td>Cell 4</td>
48 |                         </tr>
49 |                     </tbody>
50 |                 </table>
51 |             `;
52 | 			const expected =
53 | 				'| Header 1 | Header 2 |\n| --- | --- |\n| Cell 1 | Cell 2 |\n| Cell 3 | Cell 4 |';
54 | 
55 | 			// Normalize whitespace for comparison
56 | 			const normalizedResult = htmlToMarkdown(html)
57 | 				.replace(/\s+/g, ' ')
58 | 				.trim();
59 | 			const normalizedExpected = expected.replace(/\s+/g, ' ').trim();
60 | 
61 | 			expect(normalizedResult).toBe(normalizedExpected);
62 | 		});
63 | 
64 | 		it('should handle strikethrough text', () => {
65 | 			const html = '<p>This is <del>deleted</del> text.</p>';
66 | 			const expected = 'This is ~~deleted~~ text.';
67 | 			expect(htmlToMarkdown(html)).toBe(expected);
68 | 		});
69 | 	});
70 | });
71 | 
```

--------------------------------------------------------------------------------
/src/cli/atlassian.search.cli.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Command } from 'commander';
 2 | import { Logger } from '../utils/logger.util.js';
 3 | import atlassianSearchController from '../controllers/atlassian.search.controller.js';
 4 | import { handleCliError } from '../utils/error-handler.util.js';
 5 | import { getDefaultWorkspace } from '../utils/workspace.util.js';
 6 | 
 7 | // Set up a logger for this module
 8 | const logger = Logger.forContext('cli/atlassian.search.cli.ts');
 9 | 
10 | /**
11 |  * Register the search commands with the CLI
12 |  * @param program The commander program to register commands with
13 |  */
14 | function register(program: Command) {
15 | 	program
16 | 		.command('search')
17 | 		.description('Search Bitbucket for content matching a query')
18 | 		.requiredOption('-q, --query <query>', 'Search query')
19 | 		.option('-w, --workspace <workspace>', 'Workspace slug')
20 | 		.option('-r, --repo <repo>', 'Repository slug (required for PR search)')
21 | 		.option(
22 | 			'-t, --type <type>',
23 | 			'Search type (code, content, repositories, pullrequests)',
24 | 			'code',
25 | 		)
26 | 		.option(
27 | 			'-c, --content-type <contentType>',
28 | 			'Content type for content search (e.g., wiki, issue)',
29 | 		)
30 | 		.option(
31 | 			'-l, --language <language>',
32 | 			'Filter code search by programming language',
33 | 		)
34 | 		.option(
35 | 			'-e, --extension <extension>',
36 | 			'Filter code search by file extension',
37 | 		)
38 | 		.option('--limit <limit>', 'Maximum number of results to return', '20')
39 | 		.option('--cursor <cursor>', 'Pagination cursor')
40 | 		.action(async (options) => {
41 | 			const methodLogger = logger.forMethod('search');
42 | 			try {
43 | 				methodLogger.debug('CLI search command called with:', options);
44 | 
45 | 				// Handle workspace
46 | 				let workspace = options.workspace;
47 | 				if (!workspace) {
48 | 					workspace = await getDefaultWorkspace();
49 | 					if (!workspace) {
50 | 						console.error(
51 | 							'Error: No workspace provided and no default workspace configured',
52 | 						);
53 | 						process.exit(1);
54 | 					}
55 | 					methodLogger.debug(`Using default workspace: ${workspace}`);
56 | 				}
57 | 
58 | 				// Prepare controller options
59 | 				const controllerOptions = {
60 | 					workspace,
61 | 					repo: options.repo,
62 | 					query: options.query,
63 | 					type: options.type,
64 | 					contentType: options.contentType,
65 | 					language: options.language,
66 | 					extension: options.extension,
67 | 					limit: options.limit
68 | 						? parseInt(options.limit, 10)
69 | 						: undefined,
70 | 					cursor: options.cursor,
71 | 				};
72 | 
73 | 				// Call the controller
74 | 				const result =
75 | 					await atlassianSearchController.search(controllerOptions);
76 | 
77 | 				// Output the result
78 | 				console.log(result.content);
79 | 			} catch (error) {
80 | 				handleCliError(error);
81 | 			}
82 | 		});
83 | }
84 | 
85 | export default { register };
86 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.search.pullrequests.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Logger } from '../utils/logger.util.js';
 2 | import { ControllerResponse } from '../types/common.types.js';
 3 | import { DEFAULT_PAGE_SIZE } from '../utils/defaults.util.js';
 4 | import atlassianPullRequestsService from '../services/vendor.atlassian.pullrequests.service.js';
 5 | import {
 6 | 	extractPaginationInfo,
 7 | 	PaginationType,
 8 | } from '../utils/pagination.util.js';
 9 | import { formatPagination } from '../utils/formatter.util.js';
10 | import { formatPullRequestsList } from './atlassian.pullrequests.formatter.js';
11 | import { ListPullRequestsParams } from '../services/vendor.atlassian.pullrequests.types.js';
12 | 
13 | /**
14 |  * Handle search for pull requests (uses PR API with query filter)
15 |  */
16 | export async function handlePullRequestSearch(
17 | 	workspaceSlug: string,
18 | 	repoSlug?: string,
19 | 	query?: string,
20 | 	limit: number = DEFAULT_PAGE_SIZE,
21 | 	cursor?: string,
22 | ): Promise<ControllerResponse> {
23 | 	const methodLogger = Logger.forContext(
24 | 		'controllers/atlassian.search.pullrequests.controller.ts',
25 | 		'handlePullRequestSearch',
26 | 	);
27 | 	methodLogger.debug('Performing pull request search');
28 | 
29 | 	if (!query) {
30 | 		return {
31 | 			content: 'Please provide a search query for pull request search.',
32 | 		};
33 | 	}
34 | 
35 | 	try {
36 | 		// Format query for the Bitbucket API - specifically target title/description
37 | 		const formattedQuery = `(title ~ "${query}" OR description ~ "${query}")`;
38 | 
39 | 		// Create the parameters for the PR service
40 | 		const params: ListPullRequestsParams = {
41 | 			workspace: workspaceSlug,
42 | 			repo_slug: repoSlug!, // Can safely use non-null assertion now that schema validation ensures it's present
43 | 			q: formattedQuery,
44 | 			pagelen: limit,
45 | 			page: cursor ? parseInt(cursor, 10) : undefined,
46 | 			sort: '-updated_on',
47 | 		};
48 | 
49 | 		methodLogger.debug('Using PR search params:', params);
50 | 
51 | 		const prData = await atlassianPullRequestsService.list(params);
52 | 		methodLogger.debug(
53 | 			`Search complete, found ${prData.values.length} matches`,
54 | 		);
55 | 
56 | 		// Extract pagination information
57 | 		const pagination = extractPaginationInfo(prData, PaginationType.PAGE);
58 | 
59 | 		// Format the search results
60 | 		const formattedPrs = formatPullRequestsList(prData);
61 | 		let finalContent = `# Pull Request Search Results\n\n${formattedPrs}`;
62 | 
63 | 		// Add pagination information if available
64 | 		if (
65 | 			pagination &&
66 | 			(pagination.hasMore || pagination.count !== undefined)
67 | 		) {
68 | 			const paginationString = formatPagination(pagination);
69 | 			finalContent += '\n\n' + paginationString;
70 | 		}
71 | 
72 | 		return {
73 | 			content: finalContent,
74 | 		};
75 | 	} catch (error) {
76 | 		methodLogger.error('Error performing pull request search:', error);
77 | 		throw error;
78 | 	}
79 | }
80 | 
```

--------------------------------------------------------------------------------
/src/utils/cli.test.util.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { spawn } from 'child_process';
  2 | import { join } from 'path';
  3 | 
  4 | /**
  5 |  * Utility for testing CLI commands with real execution
  6 |  */
  7 | export class CliTestUtil {
  8 | 	/**
  9 | 	 * Executes a CLI command and returns the result
 10 | 	 *
 11 | 	 * @param args - CLI arguments to pass to the command
 12 | 	 * @param options - Test options
 13 | 	 * @returns Promise with stdout, stderr, and exit code
 14 | 	 */
 15 | 	static async runCommand(
 16 | 		args: string[],
 17 | 		options: {
 18 | 			timeoutMs?: number;
 19 | 			env?: Record<string, string>;
 20 | 		} = {},
 21 | 	): Promise<{
 22 | 		stdout: string;
 23 | 		stderr: string;
 24 | 		exitCode: number;
 25 | 	}> {
 26 | 		// Default timeout of 30 seconds
 27 | 		const timeoutMs = options.timeoutMs || 30000;
 28 | 
 29 | 		// CLI execution path - points to the built CLI script
 30 | 		const cliPath = join(process.cwd(), 'dist', 'index.js');
 31 | 
 32 | 		return new Promise((resolve, reject) => {
 33 | 			// Set up timeout handler
 34 | 			const timeoutId = setTimeout(() => {
 35 | 				child.kill();
 36 | 				reject(new Error(`CLI command timed out after ${timeoutMs}ms`));
 37 | 			}, timeoutMs);
 38 | 
 39 | 			// Capture stdout and stderr
 40 | 			let stdout = '';
 41 | 			let stderr = '';
 42 | 
 43 | 			// Spawn the process with given arguments
 44 | 			const child = spawn('node', [cliPath, ...args], {
 45 | 				env: {
 46 | 					...process.env,
 47 | 					...options.env,
 48 | 				},
 49 | 			});
 50 | 
 51 | 			// Collect stdout data
 52 | 			child.stdout.on('data', (data) => {
 53 | 				stdout += data.toString();
 54 | 			});
 55 | 
 56 | 			// Collect stderr data
 57 | 			child.stderr.on('data', (data) => {
 58 | 				stderr += data.toString();
 59 | 			});
 60 | 
 61 | 			// Handle process completion
 62 | 			child.on('close', (exitCode) => {
 63 | 				clearTimeout(timeoutId);
 64 | 				resolve({
 65 | 					stdout,
 66 | 					stderr,
 67 | 					exitCode: exitCode ?? 0,
 68 | 				});
 69 | 			});
 70 | 
 71 | 			// Handle process errors
 72 | 			child.on('error', (err) => {
 73 | 				clearTimeout(timeoutId);
 74 | 				reject(err);
 75 | 			});
 76 | 		});
 77 | 	}
 78 | 
 79 | 	/**
 80 | 	 * Validates that stdout contains expected strings/patterns
 81 | 	 */
 82 | 	static validateOutputContains(
 83 | 		output: string,
 84 | 		expectedPatterns: (string | RegExp)[],
 85 | 	): void {
 86 | 		for (const pattern of expectedPatterns) {
 87 | 			if (typeof pattern === 'string') {
 88 | 				expect(output).toContain(pattern);
 89 | 			} else {
 90 | 				expect(output).toMatch(pattern);
 91 | 			}
 92 | 		}
 93 | 	}
 94 | 
 95 | 	/**
 96 | 	 * Validates Markdown output format
 97 | 	 */
 98 | 	static validateMarkdownOutput(output: string): void {
 99 | 		// Check for Markdown heading
100 | 		expect(output).toMatch(/^#\s.+/m);
101 | 
102 | 		// Check for markdown formatting elements like bold text, lists, etc.
103 | 		const markdownElements = [
104 | 			/\*\*.+\*\*/, // Bold text
105 | 			/-\s.+/, // List items
106 | 			/\|.+\|.+\|/, // Table rows
107 | 			/\[.+\]\(.+\)/, // Links
108 | 		];
109 | 
110 | 		expect(markdownElements.some((pattern) => pattern.test(output))).toBe(
111 | 			true,
112 | 		);
113 | 	}
114 | }
115 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.search.content.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Logger } from '../utils/logger.util.js';
 2 | import { ControllerResponse } from '../types/common.types.js';
 3 | import { DEFAULT_PAGE_SIZE } from '../utils/defaults.util.js';
 4 | import {
 5 | 	extractPaginationInfo,
 6 | 	PaginationType,
 7 | } from '../utils/pagination.util.js';
 8 | import { formatPagination } from '../utils/formatter.util.js';
 9 | import { formatContentSearchResults } from './atlassian.search.formatter.js';
10 | import { ContentType } from '../utils/atlassian.util.js';
11 | import { ContentSearchParams } from '../services/vendor.atlassian.search.types.js';
12 | import atlassianSearchService from '../services/vendor.atlassian.search.service.js';
13 | 
14 | /**
15 |  * Handle search for content (PRs, Issues, Wiki, etc.)
16 |  */
17 | export async function handleContentSearch(
18 | 	workspaceSlug: string,
19 | 	repoSlug?: string,
20 | 	query?: string,
21 | 	limit: number = DEFAULT_PAGE_SIZE,
22 | 	cursor?: string,
23 | 	contentType?: ContentType,
24 | ): Promise<ControllerResponse> {
25 | 	const methodLogger = Logger.forContext(
26 | 		'controllers/atlassian.search.content.controller.ts',
27 | 		'handleContentSearch',
28 | 	);
29 | 	methodLogger.debug('Performing content search');
30 | 
31 | 	if (!query) {
32 | 		return {
33 | 			content: 'Please provide a search query for content search.',
34 | 		};
35 | 	}
36 | 
37 | 	try {
38 | 		const params: ContentSearchParams = {
39 | 			workspaceSlug,
40 | 			query,
41 | 			limit,
42 | 			page: cursor ? parseInt(cursor, 10) : 1,
43 | 		};
44 | 
45 | 		// Add optional parameters if provided
46 | 		if (repoSlug) {
47 | 			params.repoSlug = repoSlug;
48 | 		}
49 | 
50 | 		if (contentType) {
51 | 			params.contentType = contentType;
52 | 		}
53 | 
54 | 		methodLogger.debug('Content search params:', params);
55 | 
56 | 		const searchResult = await atlassianSearchService.searchContent(params);
57 | 
58 | 		methodLogger.debug(
59 | 			`Content search complete, found ${searchResult.size} matches`,
60 | 		);
61 | 
62 | 		// Extract pagination information
63 | 		const pagination = extractPaginationInfo(
64 | 			{
65 | 				...searchResult,
66 | 				// For content search, the Bitbucket API returns values and size differently
67 | 				// We need to map it to a format that extractPaginationInfo can understand
68 | 				page: params.page,
69 | 				pagelen: limit,
70 | 			},
71 | 			PaginationType.PAGE,
72 | 		);
73 | 
74 | 		// Format the search results
75 | 		const formattedResults = formatContentSearchResults(
76 | 			searchResult,
77 | 			contentType,
78 | 		);
79 | 
80 | 		// Add pagination information if available
81 | 		let finalContent = formattedResults;
82 | 		if (
83 | 			pagination &&
84 | 			(pagination.hasMore || pagination.count !== undefined)
85 | 		) {
86 | 			const paginationString = formatPagination(pagination);
87 | 			finalContent += '\n\n' + paginationString;
88 | 		}
89 | 
90 | 		return {
91 | 			content: finalContent,
92 | 		};
93 | 	} catch (searchError) {
94 | 		methodLogger.error('Error performing content search:', searchError);
95 | 		throw searchError;
96 | 	}
97 | }
98 | 
```

--------------------------------------------------------------------------------
/src/utils/markdown.util.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Markdown utility functions for converting HTML to Markdown
  3 |  * Uses Turndown library for HTML to Markdown conversion
  4 |  *
  5 |  * @see https://github.com/mixmark-io/turndown
  6 |  */
  7 | 
  8 | import TurndownService from 'turndown';
  9 | import { Logger } from './logger.util.js';
 10 | 
 11 | // Create a file-level logger for the module
 12 | const markdownLogger = Logger.forContext('utils/markdown.util.ts');
 13 | 
 14 | // DOM type definitions
 15 | interface HTMLElement {
 16 | 	nodeName: string;
 17 | 	parentNode?: Node;
 18 | 	childNodes: NodeListOf<Node>;
 19 | }
 20 | 
 21 | interface Node {
 22 | 	tagName?: string;
 23 | 	childNodes: NodeListOf<Node>;
 24 | 	parentNode?: Node;
 25 | }
 26 | 
 27 | interface NodeListOf<T> extends Array<T> {
 28 | 	length: number;
 29 | }
 30 | 
 31 | // Create a singleton instance of TurndownService with default options
 32 | const turndownService = new TurndownService({
 33 | 	headingStyle: 'atx', // Use # style headings
 34 | 	bulletListMarker: '-', // Use - for bullet lists
 35 | 	codeBlockStyle: 'fenced', // Use ``` for code blocks
 36 | 	emDelimiter: '_', // Use _ for emphasis
 37 | 	strongDelimiter: '**', // Use ** for strong
 38 | 	linkStyle: 'inlined', // Use [text](url) for links
 39 | 	linkReferenceStyle: 'full', // Use [text][id] + [id]: url for reference links
 40 | });
 41 | 
 42 | // Add custom rule for strikethrough
 43 | turndownService.addRule('strikethrough', {
 44 | 	filter: (node: HTMLElement) => {
 45 | 		return (
 46 | 			node.nodeName.toLowerCase() === 'del' ||
 47 | 			node.nodeName.toLowerCase() === 's' ||
 48 | 			node.nodeName.toLowerCase() === 'strike'
 49 | 		);
 50 | 	},
 51 | 	replacement: (content: string): string => `~~${content}~~`,
 52 | });
 53 | 
 54 | // Add custom rule for tables to improve table formatting
 55 | turndownService.addRule('tableCell', {
 56 | 	filter: ['th', 'td'],
 57 | 	replacement: (content: string, _node: TurndownService.Node): string => {
 58 | 		return ` ${content} |`;
 59 | 	},
 60 | });
 61 | 
 62 | turndownService.addRule('tableRow', {
 63 | 	filter: 'tr',
 64 | 	replacement: (content: string, node: TurndownService.Node): string => {
 65 | 		let output = `|${content}\n`;
 66 | 
 67 | 		// If this is the first row in a table head, add the header separator row
 68 | 		if (
 69 | 			node.parentNode &&
 70 | 			'tagName' in node.parentNode &&
 71 | 			node.parentNode.tagName === 'THEAD'
 72 | 		) {
 73 | 			const cellCount = node.childNodes.length;
 74 | 			output += '|' + ' --- |'.repeat(cellCount) + '\n';
 75 | 		}
 76 | 
 77 | 		return output;
 78 | 	},
 79 | });
 80 | 
 81 | /**
 82 |  * Convert HTML content to Markdown
 83 |  *
 84 |  * @param html - The HTML content to convert
 85 |  * @returns The converted Markdown content
 86 |  */
 87 | export function htmlToMarkdown(html: string): string {
 88 | 	if (!html || html.trim() === '') {
 89 | 		return '';
 90 | 	}
 91 | 
 92 | 	try {
 93 | 		const markdown = turndownService.turndown(html);
 94 | 		return markdown;
 95 | 	} catch (error) {
 96 | 		markdownLogger.error('Error converting HTML to Markdown:', error);
 97 | 		// Return the original HTML if conversion fails
 98 | 		return html;
 99 | 	}
100 | }
101 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci-semantic-release.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI - Semantic Release
 2 | 
 3 | # This workflow is triggered on every push to the main branch
 4 | # It analyzes commits and automatically releases a new version when needed
 5 | on:
 6 |     push:
 7 |         branches: [main]
 8 | 
 9 | jobs:
10 |     release:
11 |         name: Semantic Release
12 |         runs-on: ubuntu-latest
13 |         # Permissions needed for creating releases, updating issues, and publishing packages
14 |         permissions:
15 |             contents: write # Needed to create releases and tags
16 |             issues: write # Needed to comment on issues
17 |             pull-requests: write # Needed to comment on pull requests
18 |             # packages permission removed as we're not using GitHub Packages
19 |         steps:
20 |             # Step 1: Check out the full Git history for proper versioning
21 |             - name: Checkout
22 |               uses: actions/checkout@v5
23 |               with:
24 |                   fetch-depth: 0 # Fetches all history for all branches and tags
25 | 
26 |             # Step 2: Setup Node.js environment
27 |             - name: Setup Node.js
28 |               uses: actions/setup-node@v5
29 |               with:
30 |                   node-version: 22 # Using Node.js 22
31 |                   cache: 'npm' # Enable npm caching
32 | 
33 |             # Step 3: Install dependencies with clean install
34 |             - name: Install dependencies
35 |               run: npm ci # Clean install preserving package-lock.json
36 | 
37 |             # Step 4: Build the package
38 |             - name: Build
39 |               run: npm run build # Compiles TypeScript to JavaScript
40 | 
41 |             # Step 5: Ensure executable permissions
42 |             - name: Set executable permissions
43 |               run: chmod +x dist/index.js
44 | 
45 |             # Step 6: Run tests to ensure quality
46 |             - name: Verify tests
47 |               run: npm test # Runs Jest tests
48 | 
49 |             # Step 7: Configure Git identity for releases
50 |             - name: Configure Git User
51 |               run: |
52 |                   git config --global user.email "github-actions[bot]@users.noreply.github.com"
53 |                   git config --global user.name "github-actions[bot]"
54 | 
55 |             # Step 8: Run semantic-release to analyze commits and publish to npm
56 |             - name: Semantic Release
57 |               id: semantic
58 |               env:
59 |                   # Tokens needed for GitHub and npm authentication
60 |                   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For creating releases and commenting
61 |                   NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # For publishing to npm
62 |               run: |
63 |                   echo "Running semantic-release for version bump and npm publishing"
64 |                   npx semantic-release
65 | 
66 |                   # Note: GitHub Packages publishing has been removed
67 | 
```

--------------------------------------------------------------------------------
/src/tools/atlassian.pullrequests.types.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { CreatePullRequestCommentToolArgs } from './atlassian.pullrequests.types';
 2 | 
 3 | describe('Atlassian Pull Requests Tool Types', () => {
 4 | 	describe('CreatePullRequestCommentToolArgs Schema', () => {
 5 | 		it('should accept valid parentId parameter for comment replies', () => {
 6 | 			const validArgs = {
 7 | 				repoSlug: 'test-repo',
 8 | 				prId: '123',
 9 | 				content: 'This is a reply to another comment',
10 | 				parentId: '456',
11 | 			};
12 | 
13 | 			const result =
14 | 				CreatePullRequestCommentToolArgs.safeParse(validArgs);
15 | 			expect(result.success).toBe(true);
16 | 			if (result.success) {
17 | 				expect(result.data.parentId).toBe('456');
18 | 				expect(result.data.repoSlug).toBe('test-repo');
19 | 				expect(result.data.prId).toBe('123');
20 | 				expect(result.data.content).toBe(
21 | 					'This is a reply to another comment',
22 | 				);
23 | 			}
24 | 		});
25 | 
26 | 		it('should work without parentId parameter for top-level comments', () => {
27 | 			const validArgs = {
28 | 				repoSlug: 'test-repo',
29 | 				prId: '123',
30 | 				content: 'This is a top-level comment',
31 | 			};
32 | 
33 | 			const result =
34 | 				CreatePullRequestCommentToolArgs.safeParse(validArgs);
35 | 			expect(result.success).toBe(true);
36 | 			if (result.success) {
37 | 				expect(result.data.parentId).toBeUndefined();
38 | 				expect(result.data.repoSlug).toBe('test-repo');
39 | 				expect(result.data.prId).toBe('123');
40 | 				expect(result.data.content).toBe('This is a top-level comment');
41 | 			}
42 | 		});
43 | 
44 | 		it('should accept both parentId and inline parameters together', () => {
45 | 			const validArgs = {
46 | 				repoSlug: 'test-repo',
47 | 				prId: '123',
48 | 				content: 'Reply with inline comment',
49 | 				parentId: '456',
50 | 				inline: {
51 | 					path: 'src/main.ts',
52 | 					line: 42,
53 | 				},
54 | 			};
55 | 
56 | 			const result =
57 | 				CreatePullRequestCommentToolArgs.safeParse(validArgs);
58 | 			expect(result.success).toBe(true);
59 | 			if (result.success) {
60 | 				expect(result.data.parentId).toBe('456');
61 | 				expect(result.data.inline?.path).toBe('src/main.ts');
62 | 				expect(result.data.inline?.line).toBe(42);
63 | 			}
64 | 		});
65 | 
66 | 		it('should require required fields even with parentId', () => {
67 | 			const invalidArgs = {
68 | 				parentId: '456', // parentId alone is not enough
69 | 			};
70 | 
71 | 			const result =
72 | 				CreatePullRequestCommentToolArgs.safeParse(invalidArgs);
73 | 			expect(result.success).toBe(false);
74 | 		});
75 | 
76 | 		it('should accept optional workspaceSlug with parentId', () => {
77 | 			const validArgs = {
78 | 				workspaceSlug: 'my-workspace',
79 | 				repoSlug: 'test-repo',
80 | 				prId: '123',
81 | 				content: 'Reply comment',
82 | 				parentId: '456',
83 | 			};
84 | 
85 | 			const result =
86 | 				CreatePullRequestCommentToolArgs.safeParse(validArgs);
87 | 			expect(result.success).toBe(true);
88 | 			if (result.success) {
89 | 				expect(result.data.workspaceSlug).toBe('my-workspace');
90 | 				expect(result.data.parentId).toBe('456');
91 | 			}
92 | 		});
93 | 	});
94 | });
95 | 
```

--------------------------------------------------------------------------------
/src/utils/workspace.util.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Logger } from './logger.util.js';
 2 | import { config } from './config.util.js';
 3 | import atlassianWorkspacesService from '../services/vendor.atlassian.workspaces.service.js';
 4 | import { WorkspaceMembership } from '../services/vendor.atlassian.workspaces.types.js';
 5 | 
 6 | const workspaceLogger = Logger.forContext('utils/workspace.util.ts');
 7 | 
 8 | /**
 9 |  * Cache for workspace data to avoid repeated API calls
10 |  */
11 | let cachedDefaultWorkspace: string | null = null;
12 | let cachedWorkspaces: WorkspaceMembership[] | null = null;
13 | 
14 | /**
15 |  * Get the default workspace slug
16 |  *
17 |  * This function follows this priority:
18 |  * 1. Use cached value if available
19 |  * 2. Check BITBUCKET_DEFAULT_WORKSPACE environment variable
20 |  * 3. Fetch from API and use the first workspace in the list
21 |  *
22 |  * @returns {Promise<string|null>} The default workspace slug or null if not available
23 |  */
24 | export async function getDefaultWorkspace(): Promise<string | null> {
25 | 	const methodLogger = workspaceLogger.forMethod('getDefaultWorkspace');
26 | 
27 | 	// Step 1: Return cached value if available
28 | 	if (cachedDefaultWorkspace) {
29 | 		methodLogger.debug(
30 | 			`Using cached default workspace: ${cachedDefaultWorkspace}`,
31 | 		);
32 | 		return cachedDefaultWorkspace;
33 | 	}
34 | 
35 | 	// Step 2: Check environment variable
36 | 	const envWorkspace = config.get('BITBUCKET_DEFAULT_WORKSPACE');
37 | 	if (envWorkspace) {
38 | 		methodLogger.debug(
39 | 			`Using default workspace from environment: ${envWorkspace}`,
40 | 		);
41 | 		cachedDefaultWorkspace = envWorkspace;
42 | 		return envWorkspace;
43 | 	}
44 | 
45 | 	// Step 3: Fetch from API
46 | 	methodLogger.debug('No default workspace configured, fetching from API...');
47 | 	try {
48 | 		const workspaces = await getWorkspaces();
49 | 
50 | 		if (workspaces.length > 0) {
51 | 			const defaultWorkspace = workspaces[0].workspace.slug;
52 | 			methodLogger.debug(
53 | 				`Using first workspace from API as default: ${defaultWorkspace}`,
54 | 			);
55 | 			cachedDefaultWorkspace = defaultWorkspace;
56 | 			return defaultWorkspace;
57 | 		} else {
58 | 			methodLogger.warn('No workspaces found in the account');
59 | 			return null;
60 | 		}
61 | 	} catch (error) {
62 | 		methodLogger.error('Failed to fetch default workspace', error);
63 | 		return null;
64 | 	}
65 | }
66 | 
67 | /**
68 |  * Get list of workspaces from API or cache
69 |  *
70 |  * @returns {Promise<WorkspaceMembership[]>} Array of workspace membership objects
71 |  */
72 | export async function getWorkspaces(): Promise<WorkspaceMembership[]> {
73 | 	const methodLogger = workspaceLogger.forMethod('getWorkspaces');
74 | 
75 | 	if (cachedWorkspaces) {
76 | 		methodLogger.debug(
77 | 			`Using ${cachedWorkspaces.length} cached workspaces`,
78 | 		);
79 | 		return cachedWorkspaces;
80 | 	}
81 | 
82 | 	try {
83 | 		const result = await atlassianWorkspacesService.list({
84 | 			pagelen: 10, // Limit to first 10 workspaces
85 | 		});
86 | 
87 | 		if (result.values) {
88 | 			cachedWorkspaces = result.values;
89 | 			methodLogger.debug(`Cached ${result.values.length} workspaces`);
90 | 			return result.values;
91 | 		} else {
92 | 			return [];
93 | 		}
94 | 	} catch (error) {
95 | 		methodLogger.error('Failed to fetch workspaces list', error);
96 | 		return [];
97 | 	}
98 | }
99 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.pullrequests.get.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ControllerResponse } from '../types/common.types.js';
 2 | import { GetPullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
 3 | import { GetPullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
 4 | import {
 5 | 	atlassianPullRequestsService,
 6 | 	Logger,
 7 | 	handleControllerError,
 8 | 	formatPullRequestDetails,
 9 | 	applyDefaults,
10 | 	getDefaultWorkspace,
11 | } from './atlassian.pullrequests.base.controller.js';
12 | 
13 | /**
14 |  * Get detailed information about a specific Bitbucket pull request
15 |  * @param options - Options including workspace slug, repo slug, and pull request ID
16 |  * @returns Promise with formatted pull request details as Markdown content
17 |  */
18 | async function get(
19 | 	options: GetPullRequestToolArgsType,
20 | ): Promise<ControllerResponse> {
21 | 	const methodLogger = Logger.forContext(
22 | 		'controllers/atlassian.pullrequests.get.controller.ts',
23 | 		'get',
24 | 	);
25 | 
26 | 	try {
27 | 		// Apply default values if needed
28 | 		const mergedOptions = applyDefaults<GetPullRequestToolArgsType>(
29 | 			options,
30 | 			{}, // No defaults required for this operation
31 | 		);
32 | 
33 | 		// Handle optional workspaceSlug - get default if not provided
34 | 		if (!mergedOptions.workspaceSlug) {
35 | 			methodLogger.debug(
36 | 				'No workspace provided, fetching default workspace',
37 | 			);
38 | 			const defaultWorkspace = await getDefaultWorkspace();
39 | 			if (!defaultWorkspace) {
40 | 				throw new Error(
41 | 					'Could not determine a default workspace. Please provide a workspaceSlug.',
42 | 				);
43 | 			}
44 | 			mergedOptions.workspaceSlug = defaultWorkspace;
45 | 			methodLogger.debug(
46 | 				`Using default workspace: ${mergedOptions.workspaceSlug}`,
47 | 			);
48 | 		}
49 | 
50 | 		const { workspaceSlug, repoSlug, prId } = mergedOptions;
51 | 
52 | 		// Validate required parameters
53 | 		if (!workspaceSlug || !repoSlug || !prId) {
54 | 			throw new Error(
55 | 				'Workspace slug, repository slug, and pull request ID are required',
56 | 			);
57 | 		}
58 | 
59 | 		methodLogger.debug(
60 | 			`Getting pull request details for ${workspaceSlug}/${repoSlug}/${prId}`,
61 | 		);
62 | 
63 | 		// Map controller options to service parameters
64 | 		const serviceParams: GetPullRequestParams = {
65 | 			workspace: workspaceSlug,
66 | 			repo_slug: repoSlug,
67 | 			pull_request_id: parseInt(prId, 10),
68 | 		};
69 | 
70 | 		// Get PR details from the service
71 | 		const pullRequestData =
72 | 			await atlassianPullRequestsService.get(serviceParams);
73 | 
74 | 		methodLogger.debug('Retrieved pull request details', {
75 | 			id: pullRequestData.id,
76 | 			title: pullRequestData.title,
77 | 			state: pullRequestData.state,
78 | 		});
79 | 
80 | 		// Format the pull request details using the formatter
81 | 		const formattedContent = formatPullRequestDetails(pullRequestData);
82 | 
83 | 		return {
84 | 			content: formattedContent,
85 | 		};
86 | 	} catch (error) {
87 | 		// Use the standardized error handler
88 | 		throw handleControllerError(error, {
89 | 			entityType: 'Pull Request',
90 | 			operation: 'retrieving details',
91 | 			source: 'controllers/atlassian.pullrequests.get.controller.ts@get',
92 | 			additionalInfo: { options },
93 | 		});
94 | 	}
95 | }
96 | 
97 | // Export the controller functions
98 | export default { get };
99 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.search.repositories.controller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Logger } from '../utils/logger.util.js';
  2 | import { ControllerResponse } from '../types/common.types.js';
  3 | import { DEFAULT_PAGE_SIZE } from '../utils/defaults.util.js';
  4 | import {
  5 | 	extractPaginationInfo,
  6 | 	PaginationType,
  7 | } from '../utils/pagination.util.js';
  8 | import { formatPagination } from '../utils/formatter.util.js';
  9 | import { formatRepositoriesList } from './atlassian.repositories.formatter.js';
 10 | import { RepositoriesResponse } from '../services/vendor.atlassian.repositories.types.js';
 11 | import {
 12 | 	fetchAtlassian,
 13 | 	getAtlassianCredentials,
 14 | } from '../utils/transport.util.js';
 15 | 
 16 | /**
 17 |  * Handle search for repositories (limited functionality in the API)
 18 |  */
 19 | export async function handleRepositorySearch(
 20 | 	workspaceSlug: string,
 21 | 	_repoSlug?: string, // Renamed to indicate it's intentionally unused
 22 | 	query?: string,
 23 | 	limit: number = DEFAULT_PAGE_SIZE,
 24 | 	cursor?: string,
 25 | ): Promise<ControllerResponse> {
 26 | 	const methodLogger = Logger.forContext(
 27 | 		'controllers/atlassian.search.repositories.controller.ts',
 28 | 		'handleRepositorySearch',
 29 | 	);
 30 | 	methodLogger.debug('Performing repository search');
 31 | 
 32 | 	if (!query) {
 33 | 		return {
 34 | 			content: 'Please provide a search query for repository search.',
 35 | 		};
 36 | 	}
 37 | 
 38 | 	try {
 39 | 		const credentials = getAtlassianCredentials();
 40 | 		if (!credentials) {
 41 | 			throw new Error(
 42 | 				'Atlassian credentials are required for this operation',
 43 | 			);
 44 | 		}
 45 | 
 46 | 		// Build query params
 47 | 		const queryParams = new URLSearchParams();
 48 | 
 49 | 		// Format the query - Bitbucket's repository API allows filtering by name/description
 50 | 		const formattedQuery = `(name ~ "${query}" OR description ~ "${query}")`;
 51 | 		queryParams.set('q', formattedQuery);
 52 | 
 53 | 		// Add pagination parameters
 54 | 		queryParams.set('pagelen', limit.toString());
 55 | 		if (cursor) {
 56 | 			queryParams.set('page', cursor);
 57 | 		}
 58 | 
 59 | 		// Sort by most recently updated
 60 | 		queryParams.set('sort', '-updated_on');
 61 | 
 62 | 		// Use the repositories endpoint to search
 63 | 		const path = `/2.0/repositories/${workspaceSlug}?${queryParams.toString()}`;
 64 | 
 65 | 		methodLogger.debug(`Sending repository search request: ${path}`);
 66 | 
 67 | 		const searchData = await fetchAtlassian<RepositoriesResponse>(
 68 | 			credentials,
 69 | 			path,
 70 | 		);
 71 | 
 72 | 		methodLogger.debug(
 73 | 			`Search complete, found ${searchData.values?.length || 0} matches`,
 74 | 		);
 75 | 
 76 | 		// Extract pagination information
 77 | 		const pagination = extractPaginationInfo(
 78 | 			searchData,
 79 | 			PaginationType.PAGE,
 80 | 		);
 81 | 
 82 | 		// Format the search results
 83 | 		const formattedRepos = formatRepositoriesList(searchData);
 84 | 		let finalContent = `# Repository Search Results\n\n${formattedRepos}`;
 85 | 
 86 | 		// Add pagination information if available
 87 | 		if (
 88 | 			pagination &&
 89 | 			(pagination.hasMore || pagination.count !== undefined)
 90 | 		) {
 91 | 			const paginationString = formatPagination(pagination);
 92 | 			finalContent += '\n\n' + paginationString;
 93 | 		}
 94 | 
 95 | 		return {
 96 | 			content: finalContent,
 97 | 		};
 98 | 	} catch (searchError) {
 99 | 		methodLogger.error('Error performing repository search:', searchError);
100 | 		throw searchError;
101 | 	}
102 | }
103 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.pullrequests.approve.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ControllerResponse } from '../types/common.types.js';
 2 | import { ApprovePullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
 3 | import { ApprovePullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
 4 | import {
 5 | 	atlassianPullRequestsService,
 6 | 	Logger,
 7 | 	handleControllerError,
 8 | 	applyDefaults,
 9 | 	getDefaultWorkspace,
10 | } from './atlassian.pullrequests.base.controller.js';
11 | 
12 | /**
13 |  * Approve a pull request in Bitbucket
14 |  * @param options - Options including workspace slug, repo slug, and pull request ID
15 |  * @returns Promise with formatted approval confirmation as Markdown content
16 |  */
17 | async function approve(
18 | 	options: ApprovePullRequestToolArgsType,
19 | ): Promise<ControllerResponse> {
20 | 	const methodLogger = Logger.forContext(
21 | 		'controllers/atlassian.pullrequests.approve.controller.ts',
22 | 		'approve',
23 | 	);
24 | 
25 | 	try {
26 | 		// Apply defaults if needed (none for this operation)
27 | 		const mergedOptions = applyDefaults<ApprovePullRequestToolArgsType>(
28 | 			options,
29 | 			{},
30 | 		);
31 | 
32 | 		// Handle optional workspaceSlug - get default if not provided
33 | 		if (!mergedOptions.workspaceSlug) {
34 | 			methodLogger.debug(
35 | 				'No workspace provided, fetching default workspace',
36 | 			);
37 | 			const defaultWorkspace = await getDefaultWorkspace();
38 | 			if (!defaultWorkspace) {
39 | 				throw new Error(
40 | 					'Could not determine a default workspace. Please provide a workspaceSlug.',
41 | 				);
42 | 			}
43 | 			mergedOptions.workspaceSlug = defaultWorkspace;
44 | 			methodLogger.debug(
45 | 				`Using default workspace: ${mergedOptions.workspaceSlug}`,
46 | 			);
47 | 		}
48 | 
49 | 		methodLogger.debug(
50 | 			`Approving pull request ${mergedOptions.pullRequestId} in ${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}`,
51 | 		);
52 | 
53 | 		// Prepare service parameters
54 | 		const serviceParams: ApprovePullRequestParams = {
55 | 			workspace: mergedOptions.workspaceSlug,
56 | 			repo_slug: mergedOptions.repoSlug,
57 | 			pull_request_id: mergedOptions.pullRequestId,
58 | 		};
59 | 
60 | 		// Call service to approve the pull request
61 | 		const participant =
62 | 			await atlassianPullRequestsService.approve(serviceParams);
63 | 
64 | 		methodLogger.debug(
65 | 			`Successfully approved pull request ${mergedOptions.pullRequestId}`,
66 | 		);
67 | 
68 | 		// Format the response
69 | 		const content = `# Pull Request Approved ✅
70 | 
71 | **Pull Request ID:** ${mergedOptions.pullRequestId}
72 | **Repository:** \`${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}\`
73 | **Approved by:** ${participant.user.display_name || participant.user.nickname || 'Unknown User'}
74 | **Status:** ${participant.state}
75 | **Participated on:** ${new Date(participant.participated_on).toLocaleString()}
76 | 
77 | The pull request has been successfully approved and is now ready for merge (pending any other required approvals or checks).`;
78 | 
79 | 		return {
80 | 			content: content,
81 | 		};
82 | 	} catch (error) {
83 | 		throw handleControllerError(error, {
84 | 			entityType: 'Pull Request',
85 | 			operation: 'approving',
86 | 			source: 'controllers/atlassian.pullrequests.approve.controller.ts@approve',
87 | 			additionalInfo: { options },
88 | 		});
89 | 	}
90 | }
91 | 
92 | export default { approve };
93 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.pullrequests.reject.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ControllerResponse } from '../types/common.types.js';
 2 | import { RejectPullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
 3 | import { RejectPullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
 4 | import {
 5 | 	atlassianPullRequestsService,
 6 | 	Logger,
 7 | 	handleControllerError,
 8 | 	applyDefaults,
 9 | 	getDefaultWorkspace,
10 | } from './atlassian.pullrequests.base.controller.js';
11 | 
12 | /**
13 |  * Request changes on a pull request in Bitbucket
14 |  * @param options - Options including workspace slug, repo slug, and pull request ID
15 |  * @returns Promise with formatted rejection confirmation as Markdown content
16 |  */
17 | async function reject(
18 | 	options: RejectPullRequestToolArgsType,
19 | ): Promise<ControllerResponse> {
20 | 	const methodLogger = Logger.forContext(
21 | 		'controllers/atlassian.pullrequests.reject.controller.ts',
22 | 		'reject',
23 | 	);
24 | 
25 | 	try {
26 | 		// Apply defaults if needed (none for this operation)
27 | 		const mergedOptions = applyDefaults<RejectPullRequestToolArgsType>(
28 | 			options,
29 | 			{},
30 | 		);
31 | 
32 | 		// Handle optional workspaceSlug - get default if not provided
33 | 		if (!mergedOptions.workspaceSlug) {
34 | 			methodLogger.debug(
35 | 				'No workspace provided, fetching default workspace',
36 | 			);
37 | 			const defaultWorkspace = await getDefaultWorkspace();
38 | 			if (!defaultWorkspace) {
39 | 				throw new Error(
40 | 					'Could not determine a default workspace. Please provide a workspaceSlug.',
41 | 				);
42 | 			}
43 | 			mergedOptions.workspaceSlug = defaultWorkspace;
44 | 			methodLogger.debug(
45 | 				`Using default workspace: ${mergedOptions.workspaceSlug}`,
46 | 			);
47 | 		}
48 | 
49 | 		methodLogger.debug(
50 | 			`Requesting changes on pull request ${mergedOptions.pullRequestId} in ${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}`,
51 | 		);
52 | 
53 | 		// Prepare service parameters
54 | 		const serviceParams: RejectPullRequestParams = {
55 | 			workspace: mergedOptions.workspaceSlug,
56 | 			repo_slug: mergedOptions.repoSlug,
57 | 			pull_request_id: mergedOptions.pullRequestId,
58 | 		};
59 | 
60 | 		// Call service to request changes on the pull request
61 | 		const participant =
62 | 			await atlassianPullRequestsService.reject(serviceParams);
63 | 
64 | 		methodLogger.debug(
65 | 			`Successfully requested changes on pull request ${mergedOptions.pullRequestId}`,
66 | 		);
67 | 
68 | 		// Format the response
69 | 		const content = `# Changes Requested 🔄
70 | 
71 | **Pull Request ID:** ${mergedOptions.pullRequestId}
72 | **Repository:** \`${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}\`
73 | **Requested by:** ${participant.user.display_name || participant.user.nickname || 'Unknown User'}
74 | **Status:** ${participant.state}
75 | **Participated on:** ${new Date(participant.participated_on).toLocaleString()}
76 | 
77 | Changes have been requested on this pull request. The author should address the feedback before the pull request can be merged.`;
78 | 
79 | 		return {
80 | 			content: content,
81 | 		};
82 | 	} catch (error) {
83 | 		throw handleControllerError(error, {
84 | 			entityType: 'Pull Request',
85 | 			operation: 'requesting changes on',
86 | 			source: 'controllers/atlassian.pullrequests.reject.controller.ts@reject',
87 | 			additionalInfo: { options },
88 | 		});
89 | 	}
90 | }
91 | 
92 | export default { reject };
93 | 
```

--------------------------------------------------------------------------------
/src/utils/path.util.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as path from 'path';
  2 | import { pathToString, isPathInHome, formatDisplayPath } from './path.util.js';
  3 | 
  4 | describe('Path Utilities', () => {
  5 | 	describe('pathToString', () => {
  6 | 		it('should convert string paths correctly', () => {
  7 | 			expect(pathToString('/test/path')).toBe('/test/path');
  8 | 		});
  9 | 
 10 | 		it('should join array paths correctly', () => {
 11 | 			expect(pathToString(['/test', 'path'])).toBe(
 12 | 				path.join('/test', 'path'),
 13 | 			);
 14 | 		});
 15 | 
 16 | 		it('should handle URL objects', () => {
 17 | 			expect(pathToString(new URL('file:///test/path'))).toBe(
 18 | 				'/test/path',
 19 | 			);
 20 | 		});
 21 | 
 22 | 		it('should handle objects with toString', () => {
 23 | 			expect(pathToString({ toString: () => '/test/path' })).toBe(
 24 | 				'/test/path',
 25 | 			);
 26 | 		});
 27 | 
 28 | 		it('should convert null or undefined to empty string', () => {
 29 | 			expect(pathToString(null)).toBe('');
 30 | 			expect(pathToString(undefined)).toBe('');
 31 | 		});
 32 | 	});
 33 | 
 34 | 	describe('isPathInHome', () => {
 35 | 		const originalHome = process.env.HOME;
 36 | 		const originalUserProfile = process.env.USERPROFILE;
 37 | 
 38 | 		beforeEach(() => {
 39 | 			// Set a mock home directory for testing
 40 | 			process.env.HOME = '/mock/home';
 41 | 			process.env.USERPROFILE = '/mock/home';
 42 | 		});
 43 | 
 44 | 		afterEach(() => {
 45 | 			// Restore the original environment variables
 46 | 			process.env.HOME = originalHome;
 47 | 			process.env.USERPROFILE = originalUserProfile;
 48 | 		});
 49 | 
 50 | 		it('should return true for paths in home directory', () => {
 51 | 			expect(isPathInHome('/mock/home/projects')).toBe(true);
 52 | 		});
 53 | 
 54 | 		it('should return false for paths outside home directory', () => {
 55 | 			expect(isPathInHome('/tmp/projects')).toBe(false);
 56 | 		});
 57 | 
 58 | 		it('should resolve relative paths correctly', () => {
 59 | 			const cwd = process.cwd();
 60 | 			if (cwd.startsWith('/mock/home')) {
 61 | 				expect(isPathInHome('./projects')).toBe(true);
 62 | 			} else {
 63 | 				expect(isPathInHome('./projects')).toBe(false);
 64 | 			}
 65 | 		});
 66 | 	});
 67 | 
 68 | 	describe('formatDisplayPath', () => {
 69 | 		const originalHome = process.env.HOME;
 70 | 		const originalUserProfile = process.env.USERPROFILE;
 71 | 
 72 | 		beforeEach(() => {
 73 | 			// Set a mock home directory for testing
 74 | 			process.env.HOME = '/mock/home';
 75 | 			process.env.USERPROFILE = '/mock/home';
 76 | 		});
 77 | 
 78 | 		afterEach(() => {
 79 | 			// Restore the original environment variables
 80 | 			process.env.HOME = originalHome;
 81 | 			process.env.USERPROFILE = originalUserProfile;
 82 | 		});
 83 | 
 84 | 		it('should replace home directory with tilde when requested', () => {
 85 | 			expect(formatDisplayPath('/mock/home/projects', true)).toBe(
 86 | 				'~/projects',
 87 | 			);
 88 | 		});
 89 | 
 90 | 		it('should not replace home directory when not requested', () => {
 91 | 			expect(formatDisplayPath('/mock/home/projects', false)).toBe(
 92 | 				'/mock/home/projects',
 93 | 			);
 94 | 		});
 95 | 
 96 | 		it('should not modify paths outside of home directory', () => {
 97 | 			expect(formatDisplayPath('/tmp/projects', true)).toBe(
 98 | 				'/tmp/projects',
 99 | 			);
100 | 		});
101 | 
102 | 		it('should resolve relative paths', () => {
103 | 			// This will resolve to the absolute path based on current working directory
104 | 			const expected = path.resolve('./projects');
105 | 			expect(formatDisplayPath('./projects')).toBe(expected);
106 | 		});
107 | 	});
108 | });
109 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.repositories.details.controller.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import atlassianRepositoriesService from '../services/vendor.atlassian.repositories.service.js';
 2 | import atlassianPullRequestsService from '../services/vendor.atlassian.pullrequests.service.js';
 3 | import { Logger } from '../utils/logger.util.js';
 4 | import { handleControllerError } from '../utils/error-handler.util.js';
 5 | import { ControllerResponse } from '../types/common.types.js';
 6 | import { GetRepositoryToolArgsType } from '../tools/atlassian.repositories.types.js';
 7 | import { formatRepositoryDetails } from './atlassian.repositories.formatter.js';
 8 | import { getDefaultWorkspace } from '../utils/workspace.util.js';
 9 | 
10 | // Logger instance for this module
11 | const logger = Logger.forContext(
12 | 	'controllers/atlassian.repositories.details.controller.ts',
13 | );
14 | 
15 | /**
16 |  * Get details of a specific repository
17 |  *
18 |  * @param params - Parameters containing workspaceSlug and repoSlug
19 |  * @returns Promise with formatted repository details content
20 |  */
21 | export async function handleRepositoryDetails(
22 | 	params: GetRepositoryToolArgsType,
23 | ): Promise<ControllerResponse> {
24 | 	const methodLogger = logger.forMethod('handleRepositoryDetails');
25 | 
26 | 	try {
27 | 		methodLogger.debug('Getting repository details', params);
28 | 
29 | 		// Handle optional workspaceSlug
30 | 		if (!params.workspaceSlug) {
31 | 			methodLogger.debug(
32 | 				'No workspace provided, fetching default workspace',
33 | 			);
34 | 			const defaultWorkspace = await getDefaultWorkspace();
35 | 			if (!defaultWorkspace) {
36 | 				throw new Error(
37 | 					'No default workspace found. Please provide a workspace slug.',
38 | 				);
39 | 			}
40 | 			params.workspaceSlug = defaultWorkspace;
41 | 			methodLogger.debug(`Using default workspace: ${defaultWorkspace}`);
42 | 		}
43 | 
44 | 		// Call the service to get repository details
45 | 		const repoData = await atlassianRepositoriesService.get({
46 | 			workspace: params.workspaceSlug,
47 | 			repo_slug: params.repoSlug,
48 | 		});
49 | 
50 | 		// Fetch recent pull requests for this repository (most recently updated, limit to 5)
51 | 		let pullRequestsData = null;
52 | 		try {
53 | 			methodLogger.debug(
54 | 				'Fetching recent pull requests for the repository',
55 | 			);
56 | 			pullRequestsData = await atlassianPullRequestsService.list({
57 | 				workspace: params.workspaceSlug,
58 | 				repo_slug: params.repoSlug,
59 | 				state: 'OPEN', // Focus on open PRs
60 | 				sort: '-updated_on', // Sort by most recently updated
61 | 				pagelen: 5, // Limit to 5 to keep the response concise
62 | 			});
63 | 			methodLogger.debug(
64 | 				`Retrieved ${pullRequestsData.values?.length || 0} recent pull requests`,
65 | 			);
66 | 		} catch (error) {
67 | 			// Log the error but continue - this is an enhancement, not critical
68 | 			methodLogger.warn(
69 | 				'Failed to fetch recent pull requests, continuing without them',
70 | 				error,
71 | 			);
72 | 			// Do not fail the entire operation if pull requests cannot be fetched
73 | 		}
74 | 
75 | 		// Format the repository data with optional pull requests
76 | 		const content = formatRepositoryDetails(repoData, pullRequestsData);
77 | 
78 | 		return { content };
79 | 	} catch (error) {
80 | 		throw handleControllerError(error, {
81 | 			entityType: 'Repository',
82 | 			operation: 'get',
83 | 			source: 'controllers/atlassian.repositories.details.controller.ts@handleRepositoryDetails',
84 | 			additionalInfo: params,
85 | 		});
86 | 	}
87 | }
88 | 
```

--------------------------------------------------------------------------------
/src/cli/atlassian.search.cli.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { CliTestUtil } from '../utils/cli.test.util';
  2 | import { getAtlassianCredentials } from '../utils/transport.util';
  3 | 
  4 | describe('Atlassian Search CLI Commands', () => {
  5 | 	beforeAll(() => {
  6 | 		// Check if credentials are available
  7 | 		const credentials = getAtlassianCredentials();
  8 | 		if (!credentials) {
  9 | 			console.warn(
 10 | 				'WARNING: No Atlassian credentials available. Live API tests will be skipped.',
 11 | 			);
 12 | 		}
 13 | 	});
 14 | 
 15 | 	/**
 16 | 	 * Helper function to skip tests if Atlassian credentials are not available
 17 | 	 */
 18 | 	const skipIfNoCredentials = () => {
 19 | 		const credentials = getAtlassianCredentials();
 20 | 		if (!credentials) {
 21 | 			return true;
 22 | 		}
 23 | 		return false;
 24 | 	};
 25 | 
 26 | 	describe('search command', () => {
 27 | 		it('should search repositories and return success exit code', async () => {
 28 | 			if (skipIfNoCredentials()) {
 29 | 				return; // Skip silently - no credentials available
 30 | 			}
 31 | 
 32 | 			const { stdout, exitCode } = await CliTestUtil.runCommand([
 33 | 				'search',
 34 | 				'--query',
 35 | 				'test',
 36 | 			]);
 37 | 
 38 | 			expect(exitCode).toBe(0);
 39 | 			CliTestUtil.validateMarkdownOutput(stdout);
 40 | 			CliTestUtil.validateOutputContains(stdout, ['## Search Results']);
 41 | 		}, 60000);
 42 | 
 43 | 		it('should support searching with query parameter', async () => {
 44 | 			if (skipIfNoCredentials()) {
 45 | 				return; // Skip silently - no credentials available
 46 | 			}
 47 | 
 48 | 			const { stdout, exitCode } = await CliTestUtil.runCommand([
 49 | 				'search',
 50 | 				'--query',
 51 | 				'api',
 52 | 			]);
 53 | 
 54 | 			expect(exitCode).toBe(0);
 55 | 			CliTestUtil.validateMarkdownOutput(stdout);
 56 | 			CliTestUtil.validateOutputContains(stdout, ['## Search Results']);
 57 | 		}, 60000);
 58 | 
 59 | 		it('should support pagination with limit flag', async () => {
 60 | 			if (skipIfNoCredentials()) {
 61 | 				return; // Skip silently - no credentials available
 62 | 			}
 63 | 
 64 | 			const { stdout, exitCode } = await CliTestUtil.runCommand([
 65 | 				'search',
 66 | 				'--query',
 67 | 				'test',
 68 | 				'--limit',
 69 | 				'2',
 70 | 			]);
 71 | 
 72 | 			expect(exitCode).toBe(0);
 73 | 			CliTestUtil.validateMarkdownOutput(stdout);
 74 | 			// Check for pagination markers
 75 | 			CliTestUtil.validateOutputContains(stdout, [
 76 | 				/Showing \d+ results/,
 77 | 				/Next page:|No more results/,
 78 | 			]);
 79 | 		}, 60000);
 80 | 
 81 | 		it('should require the query parameter', async () => {
 82 | 			const { stderr, exitCode } = await CliTestUtil.runCommand([
 83 | 				'search',
 84 | 			]);
 85 | 
 86 | 			expect(exitCode).not.toBe(0);
 87 | 			expect(stderr).toMatch(
 88 | 				/required option|missing required|specify a query/i,
 89 | 			);
 90 | 		}, 30000);
 91 | 
 92 | 		it('should handle invalid limit value gracefully', async () => {
 93 | 			if (skipIfNoCredentials()) {
 94 | 				return; // Skip silently - no credentials available
 95 | 			}
 96 | 
 97 | 			const { stdout, exitCode } = await CliTestUtil.runCommand([
 98 | 				'search',
 99 | 				'--query',
100 | 				'test',
101 | 				'--limit',
102 | 				'not-a-number',
103 | 			]);
104 | 
105 | 			expect(exitCode).not.toBe(0);
106 | 			CliTestUtil.validateOutputContains(stdout, [
107 | 				/Error|Invalid|Failed/i,
108 | 			]);
109 | 		}, 60000);
110 | 
111 | 		it('should handle help flag correctly', async () => {
112 | 			const { stdout, exitCode } = await CliTestUtil.runCommand([
113 | 				'search',
114 | 				'--help',
115 | 			]);
116 | 
117 | 			expect(exitCode).toBe(0);
118 | 			expect(stdout).toMatch(/Usage|Options|Description/i);
119 | 			expect(stdout).toContain('search');
120 | 		}, 15000);
121 | 	});
122 | });
123 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.pullrequests.update.controller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ControllerResponse } from '../types/common.types.js';
  2 | import { UpdatePullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
  3 | import { UpdatePullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
  4 | import {
  5 | 	atlassianPullRequestsService,
  6 | 	Logger,
  7 | 	handleControllerError,
  8 | 	formatPullRequestDetails,
  9 | 	applyDefaults,
 10 | 	optimizeBitbucketMarkdown,
 11 | 	getDefaultWorkspace,
 12 | } from './atlassian.pullrequests.base.controller.js';
 13 | 
 14 | /**
 15 |  * Update an existing pull request in Bitbucket
 16 |  * @param options - Options including workspace slug, repo slug, pull request ID, title, and description
 17 |  * @returns Promise with formatted updated pull request details as Markdown content
 18 |  */
 19 | async function update(
 20 | 	options: UpdatePullRequestToolArgsType,
 21 | ): Promise<ControllerResponse> {
 22 | 	const methodLogger = Logger.forContext(
 23 | 		'controllers/atlassian.pullrequests.update.controller.ts',
 24 | 		'update',
 25 | 	);
 26 | 
 27 | 	try {
 28 | 		// Apply defaults if needed (none for this operation)
 29 | 		const mergedOptions = applyDefaults<UpdatePullRequestToolArgsType>(
 30 | 			options,
 31 | 			{},
 32 | 		);
 33 | 
 34 | 		// Handle optional workspaceSlug - get default if not provided
 35 | 		if (!mergedOptions.workspaceSlug) {
 36 | 			methodLogger.debug(
 37 | 				'No workspace provided, fetching default workspace',
 38 | 			);
 39 | 			const defaultWorkspace = await getDefaultWorkspace();
 40 | 			if (!defaultWorkspace) {
 41 | 				throw new Error(
 42 | 					'Could not determine a default workspace. Please provide a workspaceSlug.',
 43 | 				);
 44 | 			}
 45 | 			mergedOptions.workspaceSlug = defaultWorkspace;
 46 | 			methodLogger.debug(
 47 | 				`Using default workspace: ${mergedOptions.workspaceSlug}`,
 48 | 			);
 49 | 		}
 50 | 
 51 | 		// Validate that at least one field to update is provided
 52 | 		if (!mergedOptions.title && !mergedOptions.description) {
 53 | 			throw new Error(
 54 | 				'At least one field to update (title or description) must be provided',
 55 | 			);
 56 | 		}
 57 | 
 58 | 		methodLogger.debug(
 59 | 			`Updating pull request ${mergedOptions.pullRequestId} in ${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}`,
 60 | 		);
 61 | 
 62 | 		// Prepare service parameters
 63 | 		const serviceParams: UpdatePullRequestParams = {
 64 | 			workspace: mergedOptions.workspaceSlug,
 65 | 			repo_slug: mergedOptions.repoSlug,
 66 | 			pull_request_id: mergedOptions.pullRequestId,
 67 | 		};
 68 | 
 69 | 		// Add optional fields if provided
 70 | 		if (mergedOptions.title !== undefined) {
 71 | 			serviceParams.title = mergedOptions.title;
 72 | 		}
 73 | 		if (mergedOptions.description !== undefined) {
 74 | 			serviceParams.description = optimizeBitbucketMarkdown(
 75 | 				mergedOptions.description,
 76 | 			);
 77 | 		}
 78 | 
 79 | 		// Call service to update the pull request
 80 | 		const pullRequest =
 81 | 			await atlassianPullRequestsService.update(serviceParams);
 82 | 
 83 | 		methodLogger.debug(
 84 | 			`Successfully updated pull request ${pullRequest.id}`,
 85 | 		);
 86 | 
 87 | 		// Format the response
 88 | 		const content = await formatPullRequestDetails(pullRequest);
 89 | 
 90 | 		return {
 91 | 			content: `## Pull Request Updated Successfully\n\n${content}`,
 92 | 		};
 93 | 	} catch (error) {
 94 | 		throw handleControllerError(error, {
 95 | 			entityType: 'Pull Request',
 96 | 			operation: 'updating',
 97 | 			source: 'controllers/atlassian.pullrequests.update.controller.ts@update',
 98 | 			additionalInfo: { options },
 99 | 		});
100 | 	}
101 | }
102 | 
103 | export default { update };
104 | 
```

--------------------------------------------------------------------------------
/src/services/vendor.atlassian.workspaces.types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | 
  3 | /**
  4 |  * Types for Atlassian Bitbucket Workspaces API
  5 |  */
  6 | 
  7 | /**
  8 |  * Workspace type (basic object)
  9 |  */
 10 | export const WorkspaceTypeSchema = z.literal('workspace');
 11 | export type WorkspaceType = z.infer<typeof WorkspaceTypeSchema>;
 12 | 
 13 | /**
 14 |  * Workspace user object
 15 |  */
 16 | export const WorkspaceUserSchema = z.object({
 17 | 	type: z.literal('user'),
 18 | 	uuid: z.string(),
 19 | 	nickname: z.string(),
 20 | 	display_name: z.string(),
 21 | });
 22 | 
 23 | /**
 24 |  * Workspace permission type
 25 |  */
 26 | export const WorkspacePermissionSchema = z.enum([
 27 | 	'owner',
 28 | 	'collaborator',
 29 | 	'member',
 30 | ]);
 31 | 
 32 | /**
 33 |  * Workspace links object
 34 |  */
 35 | const LinkSchema = z.object({
 36 | 	href: z.string(),
 37 | 	name: z.string().optional(),
 38 | });
 39 | 
 40 | export const WorkspaceLinksSchema = z.object({
 41 | 	avatar: LinkSchema.optional(),
 42 | 	html: LinkSchema.optional(),
 43 | 	members: LinkSchema.optional(),
 44 | 	owners: LinkSchema.optional(),
 45 | 	projects: LinkSchema.optional(),
 46 | 	repositories: LinkSchema.optional(),
 47 | 	snippets: LinkSchema.optional(),
 48 | 	self: LinkSchema.optional(),
 49 | });
 50 | export type WorkspaceLinks = z.infer<typeof WorkspaceLinksSchema>;
 51 | 
 52 | /**
 53 |  * Workspace forking mode
 54 |  */
 55 | export const WorkspaceForkingModeSchema = z.enum([
 56 | 	'allow_forks',
 57 | 	'no_public_forks',
 58 | 	'no_forks',
 59 | ]);
 60 | export type WorkspaceForkingMode = z.infer<typeof WorkspaceForkingModeSchema>;
 61 | 
 62 | /**
 63 |  * Workspace object returned from the API
 64 |  */
 65 | export const WorkspaceSchema: z.ZodType<{
 66 | 	type: WorkspaceType;
 67 | 	uuid: string;
 68 | 	name: string;
 69 | 	slug: string;
 70 | 	is_private?: boolean;
 71 | 	is_privacy_enforced?: boolean;
 72 | 	forking_mode?: WorkspaceForkingMode;
 73 | 	created_on?: string;
 74 | 	updated_on?: string;
 75 | 	links: WorkspaceLinks;
 76 | }> = z.object({
 77 | 	type: WorkspaceTypeSchema,
 78 | 	uuid: z.string(),
 79 | 	name: z.string(),
 80 | 	slug: z.string(),
 81 | 	is_private: z.boolean().optional(),
 82 | 	is_privacy_enforced: z.boolean().optional(),
 83 | 	forking_mode: WorkspaceForkingModeSchema.optional(),
 84 | 	created_on: z.string().optional(),
 85 | 	updated_on: z.string().optional(),
 86 | 	links: WorkspaceLinksSchema,
 87 | });
 88 | 
 89 | /**
 90 |  * Workspace membership object
 91 |  */
 92 | export const WorkspaceMembershipSchema = z.object({
 93 | 	type: z.literal('workspace_membership'),
 94 | 	permission: WorkspacePermissionSchema,
 95 | 	last_accessed: z.string().optional(),
 96 | 	added_on: z.string().optional(),
 97 | 	user: WorkspaceUserSchema,
 98 | 	workspace: WorkspaceSchema,
 99 | });
100 | export type WorkspaceMembership = z.infer<typeof WorkspaceMembershipSchema>;
101 | 
102 | /**
103 |  * Extended workspace object with optional fields
104 |  * @remarks Currently identical to Workspace, but allows for future extension
105 |  */
106 | export const WorkspaceDetailedSchema = WorkspaceSchema;
107 | export type WorkspaceDetailed = z.infer<typeof WorkspaceDetailedSchema>;
108 | 
109 | /**
110 |  * Parameters for listing workspaces
111 |  */
112 | export const ListWorkspacesParamsSchema = z.object({
113 | 	q: z.string().optional(),
114 | 	page: z.number().optional(),
115 | 	pagelen: z.number().optional(),
116 | });
117 | export type ListWorkspacesParams = z.infer<typeof ListWorkspacesParamsSchema>;
118 | 
119 | /**
120 |  * API response for user permissions on workspaces
121 |  */
122 | export const WorkspacePermissionsResponseSchema = z.object({
123 | 	pagelen: z.number(),
124 | 	page: z.number(),
125 | 	size: z.number(),
126 | 	next: z.string().optional(),
127 | 	previous: z.string().optional(),
128 | 	values: z.array(WorkspaceMembershipSchema),
129 | });
130 | export type WorkspacePermissionsResponse = z.infer<
131 | 	typeof WorkspacePermissionsResponseSchema
132 | >;
133 | 
```

--------------------------------------------------------------------------------
/src/utils/diff.util.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Logger } from './logger.util.js';
 2 | 
 3 | const utilLogger = Logger.forContext('utils/diff.util.ts');
 4 | 
 5 | /**
 6 |  * Extracts a code snippet from raw unified diff content around a specific line number.
 7 |  *
 8 |  * @param diffContent - The raw unified diff content (string).
 9 |  * @param targetLineNumber - The line number (in the "new" file) to center the snippet around.
10 |  * @param contextLines - The number of lines to include before and after the target line.
11 |  * @returns The extracted code snippet as a string, or an empty string if extraction fails.
12 |  */
13 | export function extractDiffSnippet(
14 | 	diffContent: string,
15 | 	targetLineNumber: number,
16 | 	contextLines = 2,
17 | ): string {
18 | 	const methodLogger = utilLogger.forMethod('extractDiffSnippet');
19 | 	methodLogger.debug(
20 | 		`Attempting to extract snippet around line ${targetLineNumber}`,
21 | 	);
22 | 	const lines = diffContent.split('\n');
23 | 	const snippetLines: string[] = [];
24 | 	let currentNewLineNumber = 0;
25 | 	let hunkHeaderFound = false;
26 | 
27 | 	for (const line of lines) {
28 | 		if (line.startsWith('@@')) {
29 | 			// Found a hunk header, parse the starting line number of the new file
30 | 			const match = line.match(/\+([0-9]+)/); // Matches the part like "+1,10" or "+5"
31 | 			if (match && match[1]) {
32 | 				currentNewLineNumber = parseInt(match[1], 10) - 1; // -1 because we increment before checking
33 | 				hunkHeaderFound = true;
34 | 				methodLogger.debug(
35 | 					`Found hunk starting at new line number: ${currentNewLineNumber + 1}`,
36 | 				);
37 | 			} else {
38 | 				methodLogger.warn('Could not parse hunk header:', line);
39 | 				hunkHeaderFound = false; // Reset if header is unparseable
40 | 			}
41 | 			continue; // Skip the hunk header line itself
42 | 		}
43 | 
44 | 		if (!hunkHeaderFound) {
45 | 			continue; // Skip lines before the first valid hunk header
46 | 		}
47 | 
48 | 		// Track line numbers only for lines added or unchanged in the new file
49 | 		if (line.startsWith('+') || line.startsWith(' ')) {
50 | 			currentNewLineNumber++;
51 | 			// Check if the current line is within the desired context range
52 | 			if (
53 | 				currentNewLineNumber >= targetLineNumber - contextLines &&
54 | 				currentNewLineNumber <= targetLineNumber + contextLines
55 | 			) {
56 | 				// Prepend line numbers for context, marking the target line
57 | 				const prefix =
58 | 					currentNewLineNumber === targetLineNumber ? '>' : ' ';
59 | 				// Add the line, removing the diff marker (+ or space)
60 | 				snippetLines.push(
61 | 					`${prefix} ${currentNewLineNumber.toString().padStart(4)}: ${line.substring(1)}`,
62 | 				);
63 | 			}
64 | 		} else if (line.startsWith('-')) {
65 | 			// Lines only in the old file don't increment the new file line number
66 | 			// but can be included for context if they fall within the range calculation *based on previous new lines*
67 | 			// This is complex logic, for now, we only show '+' and ' ' lines for simplicity.
68 | 			// Future enhancement: Show '-' lines that are adjacent to the target context.
69 | 		}
70 | 
71 | 		// Optimization: if we've passed the target context range, stop processing
72 | 		if (currentNewLineNumber > targetLineNumber + contextLines) {
73 | 			methodLogger.debug(
74 | 				`Passed target context range (current: ${currentNewLineNumber}, target: ${targetLineNumber}). Stopping search.`,
75 | 			);
76 | 			break;
77 | 		}
78 | 	}
79 | 
80 | 	if (snippetLines.length === 0) {
81 | 		methodLogger.warn(
82 | 			`Could not find or extract snippet for line ${targetLineNumber}`,
83 | 		);
84 | 		return ''; // Return empty if no relevant lines found
85 | 	}
86 | 
87 | 	methodLogger.debug(
88 | 		`Successfully extracted snippet with ${snippetLines.length} lines.`,
89 | 	);
90 | 	return snippetLines.join('\n');
91 | }
92 | 
```

--------------------------------------------------------------------------------
/src/cli/atlassian.workspaces.cli.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Command } from 'commander';
  2 | import { Logger } from '../utils/logger.util.js';
  3 | import { handleCliError } from '../utils/error.util.js';
  4 | import atlassianWorkspacesController from '../controllers/atlassian.workspaces.controller.js';
  5 | 
  6 | /**
  7 |  * CLI module for managing Bitbucket workspaces.
  8 |  * Provides commands for listing workspaces and retrieving workspace details.
  9 |  * All commands require valid Atlassian credentials.
 10 |  */
 11 | 
 12 | // Create a contextualized logger for this file
 13 | const cliLogger = Logger.forContext('cli/atlassian.workspaces.cli.ts');
 14 | 
 15 | // Log CLI initialization
 16 | cliLogger.debug('Bitbucket workspaces CLI module initialized');
 17 | 
 18 | /**
 19 |  * Register Bitbucket workspaces CLI commands with the Commander program
 20 |  *
 21 |  * @param program - The Commander program instance to register commands with
 22 |  * @throws Error if command registration fails
 23 |  */
 24 | function register(program: Command): void {
 25 | 	const methodLogger = Logger.forContext(
 26 | 		'cli/atlassian.workspaces.cli.ts',
 27 | 		'register',
 28 | 	);
 29 | 	methodLogger.debug('Registering Bitbucket Workspaces CLI commands...');
 30 | 
 31 | 	registerListWorkspacesCommand(program);
 32 | 	registerGetWorkspaceCommand(program);
 33 | 
 34 | 	methodLogger.debug('CLI commands registered successfully');
 35 | }
 36 | 
 37 | /**
 38 |  * Register the command for listing Bitbucket workspaces
 39 |  *
 40 |  * @param program - The Commander program instance
 41 |  */
 42 | function registerListWorkspacesCommand(program: Command): void {
 43 | 	program
 44 | 		.command('ls-workspaces')
 45 | 		.description('List workspaces in your Bitbucket account.')
 46 | 		.option(
 47 | 			'-l, --limit <number>',
 48 | 			'Maximum number of workspaces to retrieve (1-100). Default: 25.',
 49 | 		)
 50 | 		.option(
 51 | 			'-c, --cursor <string>',
 52 | 			'Pagination cursor for retrieving the next set of results.',
 53 | 		)
 54 | 		.action(async (options) => {
 55 | 			const actionLogger = cliLogger.forMethod('ls-workspaces');
 56 | 			try {
 57 | 				actionLogger.debug('Processing command options:', options);
 58 | 
 59 | 				// Map CLI options to controller params - keep only type conversions
 60 | 				const controllerOptions = {
 61 | 					limit: options.limit
 62 | 						? parseInt(options.limit, 10)
 63 | 						: undefined,
 64 | 					cursor: options.cursor,
 65 | 				};
 66 | 
 67 | 				// Call controller directly
 68 | 				const result =
 69 | 					await atlassianWorkspacesController.list(controllerOptions);
 70 | 
 71 | 				console.log(result.content);
 72 | 			} catch (error) {
 73 | 				actionLogger.error('Operation failed:', error);
 74 | 				handleCliError(error);
 75 | 			}
 76 | 		});
 77 | }
 78 | 
 79 | /**
 80 |  * Register the command for retrieving a specific Bitbucket workspace
 81 |  *
 82 |  * @param program - The Commander program instance
 83 |  */
 84 | function registerGetWorkspaceCommand(program: Command): void {
 85 | 	program
 86 | 		.command('get-workspace')
 87 | 		.description(
 88 | 			'Get detailed information about a specific Bitbucket workspace.',
 89 | 		)
 90 | 		.requiredOption(
 91 | 			'-w, --workspace-slug <slug>',
 92 | 			'Workspace slug to retrieve. Must be a valid workspace slug from your Bitbucket account. Example: "myteam"',
 93 | 		)
 94 | 		.action(async (options) => {
 95 | 			const actionLogger = Logger.forContext(
 96 | 				'cli/atlassian.workspaces.cli.ts',
 97 | 				'get-workspace',
 98 | 			);
 99 | 			try {
100 | 				actionLogger.debug(
101 | 					`Fetching workspace: ${options.workspaceSlug}`,
102 | 				);
103 | 
104 | 				// Call controller directly with passed options
105 | 				const result = await atlassianWorkspacesController.get({
106 | 					workspaceSlug: options.workspaceSlug,
107 | 				});
108 | 
109 | 				console.log(result.content);
110 | 			} catch (error) {
111 | 				actionLogger.error('Operation failed:', error);
112 | 				handleCliError(error);
113 | 			}
114 | 		});
115 | }
116 | 
117 | export default { register };
118 | 
```

--------------------------------------------------------------------------------
/src/tools/atlassian.diff.tool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { Logger } from '../utils/logger.util.js';
  3 | import { formatErrorForMcpTool } from '../utils/error.util.js';
  4 | import diffController from '../controllers/atlassian.diff.controller.js';
  5 | import {
  6 | 	BranchDiffArgsSchema,
  7 | 	CommitDiffArgsSchema,
  8 | 	type BranchDiffArgsType,
  9 | 	type CommitDiffArgsType,
 10 | } from './atlassian.diff.types.js';
 11 | 
 12 | // Create a contextualized logger for this file
 13 | const toolLogger = Logger.forContext('tools/atlassian.diff.tool.ts');
 14 | 
 15 | // Log tool initialization
 16 | toolLogger.debug('Bitbucket diff tool initialized');
 17 | 
 18 | /**
 19 |  * Handles branch diff requests
 20 |  * @param args - Arguments for the branch diff operation
 21 |  * @returns MCP tool response
 22 |  */
 23 | async function branchDiff(args: Record<string, unknown>) {
 24 | 	const methodLogger = toolLogger.forMethod('branchDiff');
 25 | 	try {
 26 | 		methodLogger.debug('Processing branch diff tool request', args);
 27 | 
 28 | 		// Pass args directly to controller without any business logic
 29 | 		const result = await diffController.branchDiff(
 30 | 			args as BranchDiffArgsType,
 31 | 		);
 32 | 
 33 | 		methodLogger.debug(
 34 | 			'Successfully retrieved branch diff from controller',
 35 | 		);
 36 | 
 37 | 		return {
 38 | 			content: [
 39 | 				{
 40 | 					type: 'text' as const,
 41 | 					text: result.content,
 42 | 				},
 43 | 			],
 44 | 		};
 45 | 	} catch (error) {
 46 | 		methodLogger.error('Failed to retrieve branch diff', error);
 47 | 		return formatErrorForMcpTool(error);
 48 | 	}
 49 | }
 50 | 
 51 | /**
 52 |  * Handles commit diff requests
 53 |  * @param args - Arguments for the commit diff operation
 54 |  * @returns MCP tool response
 55 |  */
 56 | async function commitDiff(args: Record<string, unknown>) {
 57 | 	const methodLogger = toolLogger.forMethod('commitDiff');
 58 | 	try {
 59 | 		methodLogger.debug('Processing commit diff tool request', args);
 60 | 
 61 | 		// Pass args directly to controller without any business logic
 62 | 		const result = await diffController.commitDiff(
 63 | 			args as CommitDiffArgsType,
 64 | 		);
 65 | 
 66 | 		methodLogger.debug(
 67 | 			'Successfully retrieved commit diff from controller',
 68 | 		);
 69 | 
 70 | 		return {
 71 | 			content: [
 72 | 				{
 73 | 					type: 'text' as const,
 74 | 					text: result.content,
 75 | 				},
 76 | 			],
 77 | 		};
 78 | 	} catch (error) {
 79 | 		methodLogger.error('Failed to retrieve commit diff', error);
 80 | 		return formatErrorForMcpTool(error);
 81 | 	}
 82 | }
 83 | 
 84 | /**
 85 |  * Register all Bitbucket diff tools with the MCP server.
 86 |  */
 87 | function registerTools(server: McpServer) {
 88 | 	const registerLogger = Logger.forContext(
 89 | 		'tools/atlassian.diff.tool.ts',
 90 | 		'registerTools',
 91 | 	);
 92 | 	registerLogger.debug('Registering Diff tools...');
 93 | 
 94 | 	// Register the branch diff tool
 95 | 	server.tool(
 96 | 		'bb_diff_branches',
 97 | 		`Shows changes between branches in a repository identified by \`workspaceSlug\` and \`repoSlug\`. Compares changes in \`sourceBranch\` relative to \`destinationBranch\`. Limits the number of files to show with \`limit\`. Returns the diff as formatted Markdown showing file changes, additions, and deletions. Requires Bitbucket credentials to be configured.`,
 98 | 		BranchDiffArgsSchema.shape,
 99 | 		branchDiff,
100 | 	);
101 | 
102 | 	// Register the commit diff tool
103 | 	server.tool(
104 | 		'bb_diff_commits',
105 | 		`Shows changes between commits in a repository identified by \`workspaceSlug\` and \`repoSlug\`. Requires \`sinceCommit\` and \`untilCommit\` to identify the specific commits to compare. Returns the diff as formatted Markdown showing file changes, additions, and deletions between the commits. Requires Bitbucket credentials to be configured.`,
106 | 		CommitDiffArgsSchema.shape,
107 | 		commitDiff,
108 | 	);
109 | 
110 | 	registerLogger.debug('Successfully registered Diff tools');
111 | }
112 | 
113 | export default { registerTools };
114 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.repositories.commit.controller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import atlassianRepositoriesService from '../services/vendor.atlassian.repositories.service.js';
  2 | import { Logger } from '../utils/logger.util.js';
  3 | import { handleControllerError } from '../utils/error-handler.util.js';
  4 | import { DEFAULT_PAGE_SIZE, applyDefaults } from '../utils/defaults.util.js';
  5 | import {
  6 | 	extractPaginationInfo,
  7 | 	PaginationType,
  8 | } from '../utils/pagination.util.js';
  9 | import { formatPagination } from '../utils/formatter.util.js';
 10 | import { ControllerResponse } from '../types/common.types.js';
 11 | import { GetCommitHistoryToolArgsType } from '../tools/atlassian.repositories.types.js';
 12 | import { formatCommitHistory } from './atlassian.repositories.formatter.js';
 13 | import { ListCommitsParams } from '../services/vendor.atlassian.repositories.types.js';
 14 | import { getDefaultWorkspace } from '../utils/workspace.util.js';
 15 | 
 16 | // Logger instance for this module
 17 | const logger = Logger.forContext(
 18 | 	'controllers/atlassian.repositories.commit.controller.ts',
 19 | );
 20 | 
 21 | /**
 22 |  * Get commit history for a repository
 23 |  *
 24 |  * @param options - Options containing repository identifiers and filters
 25 |  * @returns Promise with formatted commit history content and pagination info
 26 |  */
 27 | export async function handleCommitHistory(
 28 | 	options: GetCommitHistoryToolArgsType,
 29 | ): Promise<ControllerResponse> {
 30 | 	const methodLogger = logger.forMethod('handleCommitHistory');
 31 | 
 32 | 	try {
 33 | 		methodLogger.debug('Getting commit history', options);
 34 | 
 35 | 		// Apply defaults
 36 | 		const defaults = {
 37 | 			limit: DEFAULT_PAGE_SIZE,
 38 | 		};
 39 | 		const params = applyDefaults(
 40 | 			options,
 41 | 			defaults,
 42 | 		) as GetCommitHistoryToolArgsType & {
 43 | 			limit: number;
 44 | 		};
 45 | 
 46 | 		// Handle optional workspaceSlug
 47 | 		if (!params.workspaceSlug) {
 48 | 			methodLogger.debug(
 49 | 				'No workspace provided, fetching default workspace',
 50 | 			);
 51 | 			const defaultWorkspace = await getDefaultWorkspace();
 52 | 			if (!defaultWorkspace) {
 53 | 				throw new Error(
 54 | 					'No default workspace found. Please provide a workspace slug.',
 55 | 				);
 56 | 			}
 57 | 			params.workspaceSlug = defaultWorkspace;
 58 | 			methodLogger.debug(`Using default workspace: ${defaultWorkspace}`);
 59 | 		}
 60 | 
 61 | 		const serviceParams: ListCommitsParams = {
 62 | 			workspace: params.workspaceSlug,
 63 | 			repo_slug: params.repoSlug,
 64 | 			include: params.revision,
 65 | 			path: params.path,
 66 | 			pagelen: params.limit,
 67 | 			page: params.cursor ? parseInt(params.cursor, 10) : undefined,
 68 | 		};
 69 | 
 70 | 		methodLogger.debug('Fetching commits with params:', serviceParams);
 71 | 		const commitsData =
 72 | 			await atlassianRepositoriesService.listCommits(serviceParams);
 73 | 		methodLogger.debug(
 74 | 			`Retrieved ${commitsData.values?.length || 0} commits`,
 75 | 		);
 76 | 
 77 | 		// Extract pagination info before formatting
 78 | 		const pagination = extractPaginationInfo(
 79 | 			commitsData,
 80 | 			PaginationType.PAGE,
 81 | 		);
 82 | 
 83 | 		const formattedHistory = formatCommitHistory(commitsData, {
 84 | 			revision: params.revision,
 85 | 			path: params.path,
 86 | 		});
 87 | 
 88 | 		// Create the final content by combining the formatted commit history with pagination information
 89 | 		let finalContent = formattedHistory;
 90 | 
 91 | 		// Add pagination information if available
 92 | 		if (
 93 | 			pagination &&
 94 | 			(pagination.hasMore || pagination.count !== undefined)
 95 | 		) {
 96 | 			const paginationString = formatPagination(pagination);
 97 | 			finalContent += '\n\n' + paginationString;
 98 | 		}
 99 | 
100 | 		return {
101 | 			content: finalContent,
102 | 		};
103 | 	} catch (error) {
104 | 		throw handleControllerError(error, {
105 | 			entityType: 'Commit History',
106 | 			operation: 'retrieving',
107 | 			source: 'controllers/atlassian.repositories.commit.controller.ts@handleCommitHistory',
108 | 			additionalInfo: { options },
109 | 		});
110 | 	}
111 | }
112 | 
```

--------------------------------------------------------------------------------
/src/tools/atlassian.search.tool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { Logger } from '../utils/logger.util.js';
  3 | import {
  4 | 	SearchToolArgsSchema,
  5 | 	type SearchToolArgsType,
  6 | } from './atlassian.search.types.js';
  7 | import atlassianSearchController from '../controllers/atlassian.search.controller.js';
  8 | import { formatErrorForMcpTool } from '../utils/error.util.js';
  9 | import { getDefaultWorkspace } from '../utils/workspace.util.js';
 10 | 
 11 | // Set up logger
 12 | const logger = Logger.forContext('tools/atlassian.search.tool.ts');
 13 | 
 14 | /**
 15 |  * Handle search command in MCP
 16 |  */
 17 | async function handleSearch(args: Record<string, unknown>) {
 18 | 	// Create a method-scoped logger
 19 | 	const methodLogger = logger.forMethod('handleSearch');
 20 | 
 21 | 	try {
 22 | 		methodLogger.debug('Search tool called with args:', args);
 23 | 
 24 | 		// Handle workspace similar to CLI implementation
 25 | 		let workspace = args.workspaceSlug;
 26 | 		if (!workspace) {
 27 | 			const defaultWorkspace = await getDefaultWorkspace();
 28 | 			if (!defaultWorkspace) {
 29 | 				return {
 30 | 					content: [
 31 | 						{
 32 | 							type: 'text' as const,
 33 | 							text: 'Error: No workspace provided and no default workspace configured',
 34 | 						},
 35 | 					],
 36 | 				};
 37 | 			}
 38 | 			workspace = defaultWorkspace;
 39 | 			methodLogger.debug(`Using default workspace: ${workspace}`);
 40 | 		}
 41 | 
 42 | 		// Pass args to controller with workspace added
 43 | 		const searchArgs: SearchToolArgsType = {
 44 | 			workspaceSlug: workspace as string,
 45 | 			repoSlug: args.repoSlug as string | undefined,
 46 | 			query: args.query as string,
 47 | 			scope:
 48 | 				(args.scope as
 49 | 					| 'code'
 50 | 					| 'content'
 51 | 					| 'repositories'
 52 | 					| 'pullrequests') || 'code',
 53 | 			contentType: args.contentType as string | undefined,
 54 | 			language: args.language as string | undefined,
 55 | 			extension: args.extension as string | undefined,
 56 | 			limit: args.limit as number | undefined,
 57 | 			cursor: args.cursor as string | undefined,
 58 | 		};
 59 | 
 60 | 		// Call the controller with proper parameter mapping
 61 | 		const controllerOptions = {
 62 | 			workspace: searchArgs.workspaceSlug,
 63 | 			repo: searchArgs.repoSlug,
 64 | 			query: searchArgs.query,
 65 | 			type: searchArgs.scope,
 66 | 			contentType: searchArgs.contentType,
 67 | 			language: searchArgs.language,
 68 | 			extension: searchArgs.extension,
 69 | 			limit: searchArgs.limit,
 70 | 			cursor: searchArgs.cursor,
 71 | 		};
 72 | 
 73 | 		const result = await atlassianSearchController.search(
 74 | 			controllerOptions as Parameters<
 75 | 				typeof atlassianSearchController.search
 76 | 			>[0],
 77 | 		);
 78 | 
 79 | 		// Return the result content in MCP format
 80 | 		return {
 81 | 			content: [{ type: 'text' as const, text: result.content }],
 82 | 		};
 83 | 	} catch (error) {
 84 | 		// Log the error
 85 | 		methodLogger.error('Search tool failed:', error);
 86 | 
 87 | 		// Format the error for MCP response
 88 | 		return formatErrorForMcpTool(error);
 89 | 	}
 90 | }
 91 | 
 92 | /**
 93 |  * Register the search tools with the MCP server
 94 |  */
 95 | function registerTools(server: McpServer) {
 96 | 	// Register the search tool using the schema shape
 97 | 	server.tool(
 98 | 		'bb_search',
 99 | 		'Searches Bitbucket for content matching the provided query. Use this tool to find repositories, code, pull requests, or other content in Bitbucket. Specify `scope` to narrow your search ("code", "repositories", "pullrequests", or "content"). Filter code searches by `language` or `extension`. Filter content searches by `contentType`. Only searches within the specified `workspaceSlug` and optionally within a specific `repoSlug`. Supports pagination via `limit` and `cursor`. Requires Atlassian Bitbucket credentials configured. Returns search results as Markdown.',
100 | 		SearchToolArgsSchema.shape,
101 | 		handleSearch,
102 | 	);
103 | 
104 | 	logger.debug('Successfully registered Bitbucket search tools');
105 | }
106 | 
107 | export default { registerTools };
108 | 
```

--------------------------------------------------------------------------------
/src/tools/atlassian.diff.types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | 
  3 | /**
  4 |  * Schema for the branch diff tool arguments
  5 |  */
  6 | export const BranchDiffArgsSchema = z.object({
  7 | 	/**
  8 | 	 * Workspace slug containing the repository
  9 | 	 */
 10 | 	workspaceSlug: z
 11 | 		.string()
 12 | 		.optional()
 13 | 		.describe(
 14 | 			'Workspace slug containing the repository. If not provided, the system will use your default workspace (either configured via BITBUCKET_DEFAULT_WORKSPACE or the first workspace in your account). Example: "myteam"',
 15 | 		),
 16 | 
 17 | 	/**
 18 | 	 * Repository slug containing the branches
 19 | 	 */
 20 | 	repoSlug: z
 21 | 		.string()
 22 | 		.min(1, 'Repository slug is required')
 23 | 		.describe(
 24 | 			'Repository slug containing the branches. Must be a valid repository slug in the specified workspace. Example: "project-api"',
 25 | 		),
 26 | 
 27 | 	/**
 28 | 	 * Source branch (feature branch)
 29 | 	 */
 30 | 	sourceBranch: z
 31 | 		.string()
 32 | 		.min(1, 'Source branch is required')
 33 | 		.describe(
 34 | 			'Source branch for comparison. IMPORTANT NOTE: The output displays as "destinationBranch → sourceBranch", and parameter naming can be counterintuitive. For full code diffs, try both parameter orders if initial results show only summary. Example: "feature/login-redesign"',
 35 | 		),
 36 | 
 37 | 	/**
 38 | 	 * Destination branch (target branch like main/master)
 39 | 	 */
 40 | 	destinationBranch: z
 41 | 		.string()
 42 | 		.optional()
 43 | 		.describe(
 44 | 			'Destination branch for comparison. IMPORTANT NOTE: The output displays as "destinationBranch → sourceBranch", and parameter naming can be counterintuitive. For full code diffs, try both parameter orders if initial results show only summary. If not specified, defaults to "main". Example: "develop"',
 45 | 		),
 46 | 
 47 | 	/**
 48 | 	 * Include full diff in the output
 49 | 	 */
 50 | 	includeFullDiff: z
 51 | 		.boolean()
 52 | 		.optional()
 53 | 		.describe(
 54 | 			'Whether to include the full code diff in the output. Defaults to true for rich output.',
 55 | 		),
 56 | 
 57 | 	/**
 58 | 	 * Maximum number of files to return per page
 59 | 	 */
 60 | 	limit: z
 61 | 		.number()
 62 | 		.int()
 63 | 		.positive()
 64 | 		.optional()
 65 | 		.describe('Maximum number of changed files to return in results'),
 66 | 
 67 | 	/**
 68 | 	 * Pagination cursor for retrieving additional results
 69 | 	 */
 70 | 	cursor: z
 71 | 		.number()
 72 | 		.int()
 73 | 		.positive()
 74 | 		.optional()
 75 | 		.describe('Pagination cursor for retrieving additional results'),
 76 | });
 77 | 
 78 | export type BranchDiffArgsType = z.infer<typeof BranchDiffArgsSchema>;
 79 | 
 80 | /**
 81 |  * Schema for the commit diff tool arguments
 82 |  */
 83 | export const CommitDiffArgsSchema = z.object({
 84 | 	workspaceSlug: z
 85 | 		.string()
 86 | 		.optional()
 87 | 		.describe(
 88 | 			'Workspace slug containing the repository. If not provided, the system will use your default workspace.',
 89 | 		),
 90 | 	repoSlug: z
 91 | 		.string()
 92 | 		.min(1)
 93 | 		.describe('Repository slug to compare commits in'),
 94 | 	sinceCommit: z
 95 | 		.string()
 96 | 		.min(1)
 97 | 		.describe(
 98 | 			'Base commit hash or reference. IMPORTANT NOTE: For proper results with code changes, this should be the NEWER commit (chronologically later). If you see "No changes detected", try reversing commit order.',
 99 | 		),
100 | 	untilCommit: z
101 | 		.string()
102 | 		.min(1)
103 | 		.describe(
104 | 			'Target commit hash or reference. IMPORTANT NOTE: For proper results with code changes, this should be the OLDER commit (chronologically earlier). If you see "No changes detected", try reversing commit order.',
105 | 		),
106 | 	includeFullDiff: z
107 | 		.boolean()
108 | 		.optional()
109 | 		.describe(
110 | 			'Whether to include the full code diff in the response (default: false)',
111 | 		),
112 | 	limit: z
113 | 		.number()
114 | 		.int()
115 | 		.positive()
116 | 		.optional()
117 | 		.describe('Maximum number of changed files to return in results'),
118 | 	cursor: z
119 | 		.number()
120 | 		.int()
121 | 		.positive()
122 | 		.optional()
123 | 		.describe('Pagination cursor for retrieving additional results'),
124 | });
125 | 
126 | export type CommitDiffArgsType = z.infer<typeof CommitDiffArgsSchema>;
127 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.pullrequests.create.controller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ControllerResponse } from '../types/common.types.js';
  2 | import { CreatePullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
  3 | import { CreatePullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
  4 | import {
  5 | 	atlassianPullRequestsService,
  6 | 	Logger,
  7 | 	handleControllerError,
  8 | 	formatPullRequestDetails,
  9 | 	applyDefaults,
 10 | 	optimizeBitbucketMarkdown,
 11 | 	getDefaultWorkspace,
 12 | } from './atlassian.pullrequests.base.controller.js';
 13 | 
 14 | /**
 15 |  * Create a new pull request in Bitbucket
 16 |  * @param options - Options including workspace slug, repo slug, source branch, target branch, title, etc.
 17 |  * @returns Promise with formatted pull request details as Markdown content
 18 |  */
 19 | async function add(
 20 | 	options: CreatePullRequestToolArgsType,
 21 | ): Promise<ControllerResponse> {
 22 | 	const methodLogger = Logger.forContext(
 23 | 		'controllers/atlassian.pullrequests.create.controller.ts',
 24 | 		'add',
 25 | 	);
 26 | 
 27 | 	try {
 28 | 		// Apply defaults if needed (none for this operation)
 29 | 		const mergedOptions = applyDefaults<CreatePullRequestToolArgsType>(
 30 | 			options,
 31 | 			{},
 32 | 		);
 33 | 
 34 | 		// Handle optional workspaceSlug - get default if not provided
 35 | 		if (!mergedOptions.workspaceSlug) {
 36 | 			methodLogger.debug(
 37 | 				'No workspace provided, fetching default workspace',
 38 | 			);
 39 | 			const defaultWorkspace = await getDefaultWorkspace();
 40 | 			if (!defaultWorkspace) {
 41 | 				throw new Error(
 42 | 					'Could not determine a default workspace. Please provide a workspaceSlug.',
 43 | 				);
 44 | 			}
 45 | 			mergedOptions.workspaceSlug = defaultWorkspace;
 46 | 			methodLogger.debug(
 47 | 				`Using default workspace: ${mergedOptions.workspaceSlug}`,
 48 | 			);
 49 | 		}
 50 | 
 51 | 		const {
 52 | 			workspaceSlug,
 53 | 			repoSlug,
 54 | 			title,
 55 | 			sourceBranch,
 56 | 			destinationBranch,
 57 | 			description,
 58 | 			closeSourceBranch,
 59 | 		} = mergedOptions;
 60 | 
 61 | 		// Validate required parameters
 62 | 		if (
 63 | 			!workspaceSlug ||
 64 | 			!repoSlug ||
 65 | 			!title ||
 66 | 			!sourceBranch ||
 67 | 			!destinationBranch
 68 | 		) {
 69 | 			throw new Error(
 70 | 				'Workspace slug, repository slug, title, source branch, and destination branch are required',
 71 | 			);
 72 | 		}
 73 | 
 74 | 		methodLogger.debug(
 75 | 			`Creating PR in ${workspaceSlug}/${repoSlug} from ${sourceBranch} to ${destinationBranch}`,
 76 | 			{
 77 | 				title,
 78 | 				descriptionLength: description?.length,
 79 | 				closeSourceBranch,
 80 | 			},
 81 | 		);
 82 | 
 83 | 		// Process description - optimize Markdown if provided
 84 | 		const optimizedDescription = description
 85 | 			? optimizeBitbucketMarkdown(description)
 86 | 			: undefined;
 87 | 
 88 | 		// Map controller options to service parameters
 89 | 		const serviceParams: CreatePullRequestParams = {
 90 | 			workspace: workspaceSlug,
 91 | 			repo_slug: repoSlug,
 92 | 			title,
 93 | 			source: {
 94 | 				branch: {
 95 | 					name: sourceBranch,
 96 | 				},
 97 | 			},
 98 | 			destination: {
 99 | 				branch: {
100 | 					name: destinationBranch,
101 | 				},
102 | 			},
103 | 			description: optimizedDescription,
104 | 			close_source_branch: closeSourceBranch,
105 | 		};
106 | 
107 | 		// Create the pull request through the service
108 | 		const pullRequestResult =
109 | 			await atlassianPullRequestsService.create(serviceParams);
110 | 
111 | 		methodLogger.debug('Pull request created successfully', {
112 | 			id: pullRequestResult.id,
113 | 			title: pullRequestResult.title,
114 | 			sourceBranch,
115 | 			destinationBranch,
116 | 		});
117 | 
118 | 		// Format the pull request details using the formatter
119 | 		const formattedContent = formatPullRequestDetails(pullRequestResult);
120 | 
121 | 		// Return formatted content with success message
122 | 		return {
123 | 			content: `## Pull Request Created Successfully\n\n${formattedContent}`,
124 | 		};
125 | 	} catch (error) {
126 | 		// Use the standardized error handler
127 | 		throw handleControllerError(error, {
128 | 			entityType: 'Pull Request',
129 | 			operation: 'creating',
130 | 			source: 'controllers/atlassian.pullrequests.create.controller.ts@add',
131 | 			additionalInfo: { options },
132 | 		});
133 | 	}
134 | }
135 | 
136 | // Export the controller functions
137 | export default { add };
138 | 
```

--------------------------------------------------------------------------------
/STYLE_GUIDE.md:
--------------------------------------------------------------------------------

```markdown
 1 | # MCP Server Style Guide
 2 | 
 3 | Based on the patterns observed and best practices, I recommend adopting the following consistent style guide across all your MCP servers:
 4 | 
 5 | | Element              | Convention                                                                                                                                    | Rationale / Examples                                                                                                                              |
 6 | | :------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ |
 7 | | **CLI Commands**     | `verb-noun` in `kebab-case`. Use the shortest unambiguous verb (`ls`, `get`, `create`, `add`, `exec`, `search`).                              | `ls-repos`, `get-pr`, `create-comment`, `exec-command`                                                                                            |
 8 | | **CLI Options**      | `--kebab-case`. Be specific (e.g., `--workspace-slug`, not just `--slug`).                                                                    | `--project-key-or-id`, `--source-branch`                                                                                                          |
 9 | | **MCP Tool Names**   | `<namespace>_<verb>_<noun>` in `snake_case`. Use a concise 2-4 char namespace. Avoid noun repetition.                                         | `bb_ls_repos` (Bitbucket list repos), `conf_get_page` (Confluence get page), `aws_exec_command` (AWS execute command). Avoid `ip_ip_get_details`. |
10 | | **MCP Arguments**    | `camelCase`. Suffix identifiers consistently (e.g., `Id`, `Key`, `Slug`). Avoid abbreviations unless universal.                               | `workspaceSlug`, `pullRequestId`, `sourceBranch`, `pageId`.                                                                                       |
11 | | **Boolean Args**     | Use verb prefixes for clarity (`includeXxx`, `launchBrowser`). Avoid bare adjectives (`--https`).                                             | `includeExtendedData: boolean`, `launchBrowser: boolean`                                                                                          |
12 | | **Array Args**       | Use plural names (`spaceIds`, `labels`, `statuses`).                                                                                          | `spaceIds: string[]`, `labels: string[]`                                                                                                          |
13 | | **Descriptions**     | **Start with an imperative verb.** Keep the first sentence concise (≤120 chars). Add 1-2 sentences detail. Mention pre-requisites/notes last. | `List available Confluence spaces. Filters by type, status, or query. Returns formatted list including ID, key, name.`                            |
14 | | **Arg Descriptions** | Start lowercase, explain purpose clearly. Mention defaults or constraints.                                                                    | `numeric ID of the page to retrieve (e.g., "456789"). Required.`                                                                                  |
15 | | **ID/Key Naming**    | Use consistent suffixes like `Id`, `Key`, `Slug`, `KeyOrId` where appropriate.                                                                | `pageId`, `projectKeyOrId`, `workspaceSlug`                                                                                                       |
16 | 
17 | Adopting this guide will make the tools more predictable and easier for both humans and AI agents to understand and use correctly.
18 | 
```

--------------------------------------------------------------------------------
/src/tools/atlassian.workspaces.tool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { Logger } from '../utils/logger.util.js';
  3 | import { formatErrorForMcpTool } from '../utils/error.util.js';
  4 | import {
  5 | 	ListWorkspacesToolArgs,
  6 | 	type ListWorkspacesToolArgsType,
  7 | 	type GetWorkspaceToolArgsType,
  8 | 	GetWorkspaceToolArgs,
  9 | } from './atlassian.workspaces.types.js';
 10 | 
 11 | import atlassianWorkspacesController from '../controllers/atlassian.workspaces.controller.js';
 12 | 
 13 | // Create a contextualized logger for this file
 14 | const toolLogger = Logger.forContext('tools/atlassian.workspaces.tool.ts');
 15 | 
 16 | // Log tool initialization
 17 | toolLogger.debug('Bitbucket workspaces tool initialized');
 18 | 
 19 | /**
 20 |  * MCP Tool: List Bitbucket Workspaces
 21 |  *
 22 |  * Lists Bitbucket workspaces available to the authenticated user with optional filtering.
 23 |  * Returns a formatted markdown response with workspace details.
 24 |  *
 25 |  * @param args - Tool arguments for filtering workspaces
 26 |  * @returns MCP response with formatted workspaces list
 27 |  * @throws Will return error message if workspace listing fails
 28 |  */
 29 | async function listWorkspaces(args: Record<string, unknown>) {
 30 | 	const methodLogger = Logger.forContext(
 31 | 		'tools/atlassian.workspaces.tool.ts',
 32 | 		'listWorkspaces',
 33 | 	);
 34 | 	methodLogger.debug('Listing Bitbucket workspaces with filters:', args);
 35 | 
 36 | 	try {
 37 | 		// Pass args directly to controller without any logic
 38 | 		const result = await atlassianWorkspacesController.list(
 39 | 			args as ListWorkspacesToolArgsType,
 40 | 		);
 41 | 
 42 | 		methodLogger.debug('Successfully retrieved workspaces from controller');
 43 | 
 44 | 		return {
 45 | 			content: [
 46 | 				{
 47 | 					type: 'text' as const,
 48 | 					text: result.content,
 49 | 				},
 50 | 			],
 51 | 		};
 52 | 	} catch (error) {
 53 | 		methodLogger.error('Failed to list workspaces', error);
 54 | 		return formatErrorForMcpTool(error);
 55 | 	}
 56 | }
 57 | 
 58 | /**
 59 |  * MCP Tool: Get Bitbucket Workspace Details
 60 |  *
 61 |  * Retrieves detailed information about a specific Bitbucket workspace.
 62 |  * Returns a formatted markdown response with workspace metadata.
 63 |  *
 64 |  * @param args - Tool arguments containing the workspace slug
 65 |  * @returns MCP response with formatted workspace details
 66 |  * @throws Will return error message if workspace retrieval fails
 67 |  */
 68 | async function getWorkspace(args: Record<string, unknown>) {
 69 | 	const methodLogger = Logger.forContext(
 70 | 		'tools/atlassian.workspaces.tool.ts',
 71 | 		'getWorkspace',
 72 | 	);
 73 | 	methodLogger.debug('Getting workspace details:', args);
 74 | 
 75 | 	try {
 76 | 		// Pass args directly to controller without any logic
 77 | 		const result = await atlassianWorkspacesController.get(
 78 | 			args as GetWorkspaceToolArgsType,
 79 | 		);
 80 | 
 81 | 		methodLogger.debug(
 82 | 			'Successfully retrieved workspace details from controller',
 83 | 		);
 84 | 
 85 | 		return {
 86 | 			content: [
 87 | 				{
 88 | 					type: 'text' as const,
 89 | 					text: result.content,
 90 | 				},
 91 | 			],
 92 | 		};
 93 | 	} catch (error) {
 94 | 		methodLogger.error('Failed to get workspace details', error);
 95 | 		return formatErrorForMcpTool(error);
 96 | 	}
 97 | }
 98 | 
 99 | /**
100 |  * Register all Bitbucket workspace tools with the MCP server.
101 |  */
102 | function registerTools(server: McpServer) {
103 | 	const registerLogger = Logger.forContext(
104 | 		'tools/atlassian.workspaces.tool.ts',
105 | 		'registerTools',
106 | 	);
107 | 	registerLogger.debug('Registering Workspace tools...');
108 | 
109 | 	// Register the list workspaces tool
110 | 	server.tool(
111 | 		'bb_ls_workspaces',
112 | 		`Lists workspaces within your Bitbucket account. Returns a formatted Markdown list showing workspace slugs, names, and membership role. Requires Bitbucket credentials to be configured.`,
113 | 		ListWorkspacesToolArgs.shape,
114 | 		listWorkspaces,
115 | 	);
116 | 
117 | 	// Register the get workspace details tool
118 | 	server.tool(
119 | 		'bb_get_workspace',
120 | 		`Retrieves detailed information for a workspace identified by \`workspaceSlug\`. Returns comprehensive workspace details as formatted Markdown, including membership, projects, and key metadata. Requires Bitbucket credentials to be configured.`,
121 | 		GetWorkspaceToolArgs.shape,
122 | 		getWorkspace,
123 | 	);
124 | 
125 | 	registerLogger.debug('Successfully registered Workspace tools');
126 | }
127 | 
128 | export default { registerTools };
129 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.search.controller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Logger } from '../utils/logger.util.js';
  2 | import { ContentType } from '../utils/atlassian.util.js';
  3 | import { handleCodeSearch } from './atlassian.search.code.controller.js';
  4 | import { handleContentSearch } from './atlassian.search.content.controller.js';
  5 | import { handleRepositorySearch } from './atlassian.search.repositories.controller.js';
  6 | import { handlePullRequestSearch } from './atlassian.search.pullrequests.controller.js';
  7 | import { DEFAULT_PAGE_SIZE, applyDefaults } from '../utils/defaults.util.js';
  8 | import { ControllerResponse } from '../types/common.types.js';
  9 | import { handleControllerError } from '../utils/error-handler.util.js';
 10 | 
 11 | // Logger instance for this module
 12 | const logger = Logger.forContext('controllers/atlassian.search.controller.ts');
 13 | 
 14 | /**
 15 |  * Search interface options
 16 |  */
 17 | export interface SearchOptions {
 18 | 	/** The workspace to search in */
 19 | 	workspace?: string;
 20 | 	/** The repository to search in (optional) */
 21 | 	repo?: string;
 22 | 	/** The search query */
 23 | 	query?: string;
 24 | 	/** The type of search to perform */
 25 | 	type?: string;
 26 | 	/** The content type to filter by (for content search) */
 27 | 	contentType?: string;
 28 | 	/** The language to filter by (for code search) */
 29 | 	language?: string;
 30 | 	/** File extension to filter by (for code search) */
 31 | 	extension?: string;
 32 | 	/** Maximum number of results to return */
 33 | 	limit?: number;
 34 | 	/** Pagination cursor */
 35 | 	cursor?: string;
 36 | }
 37 | 
 38 | /**
 39 |  * Perform a search across various Bitbucket data types
 40 |  *
 41 |  * @param options Search options
 42 |  * @returns Formatted search results
 43 |  */
 44 | async function search(
 45 | 	options: SearchOptions = {},
 46 | ): Promise<ControllerResponse> {
 47 | 	const methodLogger = logger.forMethod('search');
 48 | 
 49 | 	try {
 50 | 		// Apply default values
 51 | 		const defaults: Partial<SearchOptions> = {
 52 | 			type: 'code',
 53 | 			workspace: '',
 54 | 			limit: DEFAULT_PAGE_SIZE,
 55 | 		};
 56 | 		const params = applyDefaults<SearchOptions>(options, defaults);
 57 | 		methodLogger.debug('Search options (with defaults):', params);
 58 | 
 59 | 		// Validate parameters
 60 | 		if (!params.workspace) {
 61 | 			methodLogger.warn('No workspace provided for search');
 62 | 			return {
 63 | 				content: 'Error: Please provide a workspace to search in.',
 64 | 			};
 65 | 		}
 66 | 
 67 | 		// Convert content type string to enum if provided (outside the switch statement)
 68 | 		let contentTypeEnum: ContentType | undefined = undefined;
 69 | 		if (params.contentType) {
 70 | 			contentTypeEnum = params.contentType.toLowerCase() as ContentType;
 71 | 		}
 72 | 
 73 | 		// Dispatch to the appropriate search function based on type
 74 | 		switch (params.type?.toLowerCase()) {
 75 | 			case 'code':
 76 | 				return await handleCodeSearch(
 77 | 					params.workspace,
 78 | 					params.repo,
 79 | 					params.query,
 80 | 					params.limit,
 81 | 					params.cursor,
 82 | 					params.language,
 83 | 					params.extension,
 84 | 				);
 85 | 
 86 | 			case 'content':
 87 | 				return await handleContentSearch(
 88 | 					params.workspace,
 89 | 					params.repo,
 90 | 					params.query,
 91 | 					params.limit,
 92 | 					params.cursor,
 93 | 					contentTypeEnum,
 94 | 				);
 95 | 
 96 | 			case 'repos':
 97 | 			case 'repositories':
 98 | 				return await handleRepositorySearch(
 99 | 					params.workspace,
100 | 					params.repo,
101 | 					params.query,
102 | 					params.limit,
103 | 					params.cursor,
104 | 				);
105 | 
106 | 			case 'prs':
107 | 			case 'pullrequests':
108 | 				if (!params.repo) {
109 | 					return {
110 | 						content:
111 | 							'Error: Repository is required for pull request search.',
112 | 					};
113 | 				}
114 | 				return await handlePullRequestSearch(
115 | 					params.workspace,
116 | 					params.repo,
117 | 					params.query,
118 | 					params.limit,
119 | 					params.cursor,
120 | 				);
121 | 
122 | 			default:
123 | 				methodLogger.warn(`Unknown search type: ${params.type}`);
124 | 				return {
125 | 					content: `Error: Unknown search type "${params.type}". Supported types are: code, content, repositories, pullrequests.`,
126 | 				};
127 | 		}
128 | 	} catch (error) {
129 | 		// Pass the error to the handler with context
130 | 		throw handleControllerError(error, {
131 | 			entityType: 'Search',
132 | 			operation: 'search',
133 | 			source: 'controllers/atlassian.search.controller.ts@search',
134 | 			additionalInfo: options as Record<string, unknown>,
135 | 		});
136 | 	}
137 | }
138 | 
139 | export default { search };
140 | 
```

--------------------------------------------------------------------------------
/src/utils/config.util.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 | 	ErrorType,
  3 | 	McpError,
  4 | 	createApiError,
  5 | 	createAuthMissingError,
  6 | 	createAuthInvalidError,
  7 | 	createUnexpectedError,
  8 | 	ensureMcpError,
  9 | 	formatErrorForMcpTool,
 10 | 	formatErrorForMcpResource,
 11 | } from './error.util.js';
 12 | 
 13 | describe('Error Utility', () => {
 14 | 	describe('McpError', () => {
 15 | 		it('should create an error with the correct properties', () => {
 16 | 			const error = new McpError('Test error', ErrorType.API_ERROR, 404);
 17 | 
 18 | 			expect(error).toBeInstanceOf(Error);
 19 | 			expect(error).toBeInstanceOf(McpError);
 20 | 			expect(error.message).toBe('Test error');
 21 | 			expect(error.type).toBe(ErrorType.API_ERROR);
 22 | 			expect(error.statusCode).toBe(404);
 23 | 			expect(error.name).toBe('McpError');
 24 | 		});
 25 | 	});
 26 | 
 27 | 	describe('Error Factory Functions', () => {
 28 | 		it('should create auth missing error', () => {
 29 | 			const error = createAuthMissingError();
 30 | 
 31 | 			expect(error).toBeInstanceOf(McpError);
 32 | 			expect(error.type).toBe(ErrorType.AUTH_MISSING);
 33 | 			expect(error.message).toBe(
 34 | 				'Authentication credentials are missing',
 35 | 			);
 36 | 		});
 37 | 
 38 | 		it('should create auth invalid error', () => {
 39 | 			const error = createAuthInvalidError('Invalid token');
 40 | 
 41 | 			expect(error).toBeInstanceOf(McpError);
 42 | 			expect(error.type).toBe(ErrorType.AUTH_INVALID);
 43 | 			expect(error.statusCode).toBe(401);
 44 | 			expect(error.message).toBe('Invalid token');
 45 | 		});
 46 | 
 47 | 		it('should create API error', () => {
 48 | 			const originalError = new Error('Original error');
 49 | 			const error = createApiError('API failed', 500, originalError);
 50 | 
 51 | 			expect(error).toBeInstanceOf(McpError);
 52 | 			expect(error.type).toBe(ErrorType.API_ERROR);
 53 | 			expect(error.statusCode).toBe(500);
 54 | 			expect(error.message).toBe('API failed');
 55 | 			expect(error.originalError).toBe(originalError);
 56 | 		});
 57 | 
 58 | 		it('should create unexpected error', () => {
 59 | 			const error = createUnexpectedError();
 60 | 
 61 | 			expect(error).toBeInstanceOf(McpError);
 62 | 			expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR);
 63 | 			expect(error.message).toBe('An unexpected error occurred');
 64 | 		});
 65 | 	});
 66 | 
 67 | 	describe('ensureMcpError', () => {
 68 | 		it('should return the same error if it is already an McpError', () => {
 69 | 			const originalError = createApiError('Original error');
 70 | 			const error = ensureMcpError(originalError);
 71 | 
 72 | 			expect(error).toBe(originalError);
 73 | 		});
 74 | 
 75 | 		it('should wrap a standard Error', () => {
 76 | 			const originalError = new Error('Standard error');
 77 | 			const error = ensureMcpError(originalError);
 78 | 
 79 | 			expect(error).toBeInstanceOf(McpError);
 80 | 			expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR);
 81 | 			expect(error.message).toBe('Standard error');
 82 | 			expect(error.originalError).toBe(originalError);
 83 | 		});
 84 | 
 85 | 		it('should handle non-Error objects', () => {
 86 | 			const error = ensureMcpError('String error');
 87 | 
 88 | 			expect(error).toBeInstanceOf(McpError);
 89 | 			expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR);
 90 | 			expect(error.message).toBe('String error');
 91 | 		});
 92 | 	});
 93 | 
 94 | 	describe('formatErrorForMcpTool', () => {
 95 | 		it('should format an error for MCP tool response', () => {
 96 | 			const error = createApiError('API error');
 97 | 			const response = formatErrorForMcpTool(error);
 98 | 
 99 | 			expect(response).toHaveProperty('content');
100 | 			expect(response.content).toHaveLength(1);
101 | 			expect(response.content[0]).toHaveProperty('type', 'text');
102 | 			expect(response.content[0]).toHaveProperty(
103 | 				'text',
104 | 				'Error: API error',
105 | 			);
106 | 		});
107 | 	});
108 | 
109 | 	describe('formatErrorForMcpResource', () => {
110 | 		it('should format an error for MCP resource response', () => {
111 | 			const error = createApiError('API error');
112 | 			const response = formatErrorForMcpResource(error, 'test://uri');
113 | 
114 | 			expect(response).toHaveProperty('contents');
115 | 			expect(response.contents).toHaveLength(1);
116 | 			expect(response.contents[0]).toHaveProperty('uri', 'test://uri');
117 | 			expect(response.contents[0]).toHaveProperty(
118 | 				'text',
119 | 				'Error: API error',
120 | 			);
121 | 			expect(response.contents[0]).toHaveProperty(
122 | 				'mimeType',
123 | 				'text/plain',
124 | 			);
125 | 			expect(response.contents[0]).toHaveProperty(
126 | 				'description',
127 | 				'Error: API_ERROR',
128 | 			);
129 | 		});
130 | 	});
131 | });
132 | 
```

--------------------------------------------------------------------------------
/src/tools/atlassian.search.types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | 
  3 | /**
  4 |  * Pagination arguments
  5 |  * Used for pagination of search results
  6 |  */
  7 | const PaginationArgs = z.object({
  8 | 	/**
  9 | 	 * Maximum number of items to return (1-100).
 10 | 	 * Use this to control the response size.
 11 | 	 * Useful for pagination or when you only need a few results.
 12 | 	 */
 13 | 	limit: z
 14 | 		.number()
 15 | 		.min(1)
 16 | 		.max(100)
 17 | 		.optional()
 18 | 		.describe(
 19 | 			'Maximum number of results to return (1-100). Use this to control the response size. Useful for pagination or when you only need a few results.',
 20 | 		),
 21 | 
 22 | 	/**
 23 | 	 * Pagination cursor for retrieving the next set of results.
 24 | 	 * For repositories and pull requests, this is a cursor string.
 25 | 	 * For code search, this is a page number.
 26 | 	 * Use this to navigate through large result sets.
 27 | 	 */
 28 | 	cursor: z
 29 | 		.string()
 30 | 		.optional()
 31 | 		.describe(
 32 | 			'Pagination cursor for retrieving the next set of results. For repositories and pull requests, this is a cursor string. For code search, this is a page number. Use this to navigate through large result sets.',
 33 | 		),
 34 | });
 35 | 
 36 | /**
 37 |  * Bitbucket search tool arguments schema base
 38 |  */
 39 | export const SearchToolArgsBase = z
 40 | 	.object({
 41 | 		/**
 42 | 		 * Workspace slug to search in. Example: "myteam"
 43 | 		 * This maps to the CLI's "--workspace" parameter.
 44 | 		 */
 45 | 		workspaceSlug: z
 46 | 			.string()
 47 | 			.optional()
 48 | 			.describe(
 49 | 				'Workspace slug to search in. If not provided, the system will use your default workspace. Example: "myteam". Equivalent to --workspace in CLI.',
 50 | 			),
 51 | 
 52 | 		/**
 53 | 		 * Optional: Repository slug to limit search scope. Required for `pullrequests` scope. Example: "project-api"
 54 | 		 * This maps to the CLI's "--repo" parameter.
 55 | 		 */
 56 | 		repoSlug: z
 57 | 			.string()
 58 | 			.optional()
 59 | 			.describe(
 60 | 				'Optional: Repository slug to limit search scope. Required for `pullrequests` scope. Example: "project-api". Equivalent to --repo in CLI.',
 61 | 			),
 62 | 
 63 | 		/**
 64 | 		 * Search query text. Required. Will match against content based on the selected search scope.
 65 | 		 * This maps to the CLI's "--query" parameter.
 66 | 		 */
 67 | 		query: z
 68 | 			.string()
 69 | 			.min(1)
 70 | 			.describe(
 71 | 				'Search query text. Required. Will match against content based on the selected search scope. Equivalent to --query in CLI.',
 72 | 			),
 73 | 
 74 | 		/**
 75 | 		 * Search scope: "code", "content", "repositories", "pullrequests". Default: "code"
 76 | 		 * This maps to the CLI's "--type" parameter.
 77 | 		 */
 78 | 		scope: z
 79 | 			.enum(['code', 'content', 'repositories', 'pullrequests'])
 80 | 			.optional()
 81 | 			.default('code')
 82 | 			.describe(
 83 | 				'Search scope: "code", "content", "repositories", "pullrequests". Default: "code". Equivalent to --type in CLI.',
 84 | 			),
 85 | 
 86 | 		/**
 87 | 		 * Content type for content search (e.g., "wiki", "issue")
 88 | 		 * This maps to the CLI's "--content-type" parameter.
 89 | 		 */
 90 | 		contentType: z
 91 | 			.string()
 92 | 			.optional()
 93 | 			.describe(
 94 | 				'Content type for content search (e.g., "wiki", "issue"). Equivalent to --content-type in CLI.',
 95 | 			),
 96 | 
 97 | 		/**
 98 | 		 * Filter code search by language.
 99 | 		 * This maps to the CLI's "--language" parameter.
100 | 		 */
101 | 		language: z
102 | 			.string()
103 | 			.optional()
104 | 			.describe(
105 | 				'Filter code search by language. Equivalent to --language in CLI.',
106 | 			),
107 | 
108 | 		/**
109 | 		 * Filter code search by file extension.
110 | 		 * This maps to the CLI's "--extension" parameter.
111 | 		 */
112 | 		extension: z
113 | 			.string()
114 | 			.optional()
115 | 			.describe(
116 | 				'Filter code search by file extension. Equivalent to --extension in CLI.',
117 | 			),
118 | 	})
119 | 	.merge(PaginationArgs);
120 | 
121 | /**
122 |  * Bitbucket search tool arguments schema with validation
123 |  */
124 | export const SearchToolArgs = SearchToolArgsBase.superRefine((data, ctx) => {
125 | 	// Make repoSlug required when scope is 'pullrequests'
126 | 	if (data.scope === 'pullrequests' && !data.repoSlug) {
127 | 		ctx.addIssue({
128 | 			code: z.ZodIssueCode.custom,
129 | 			message: 'repoSlug is required when scope is "pullrequests"',
130 | 			path: ['repoSlug'],
131 | 		});
132 | 	}
133 | });
134 | 
135 | // Export both the schema and its shape for use with the MCP server
136 | export const SearchToolArgsSchema = SearchToolArgsBase;
137 | 
138 | export type SearchToolArgsType = z.infer<typeof SearchToolArgs>;
139 | 
```

--------------------------------------------------------------------------------
/src/controllers/atlassian.pullrequests.list.controller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ControllerResponse } from '../types/common.types.js';
  2 | import { ListPullRequestsParams } from '../services/vendor.atlassian.pullrequests.types.js';
  3 | import { ListPullRequestsToolArgsType } from '../tools/atlassian.pullrequests.types.js';
  4 | import {
  5 | 	atlassianPullRequestsService,
  6 | 	Logger,
  7 | 	handleControllerError,
  8 | 	extractPaginationInfo,
  9 | 	PaginationType,
 10 | 	formatPagination,
 11 | 	formatPullRequestsList,
 12 | 	DEFAULT_PAGE_SIZE,
 13 | 	applyDefaults,
 14 | 	getDefaultWorkspace,
 15 | } from './atlassian.pullrequests.base.controller.js';
 16 | 
 17 | /**
 18 |  * List Bitbucket pull requests with optional filtering options
 19 |  * @param options - Options for listing pull requests including workspace slug and repo slug
 20 |  * @returns Promise with formatted pull requests list content and pagination information
 21 |  */
 22 | async function list(
 23 | 	options: ListPullRequestsToolArgsType,
 24 | ): Promise<ControllerResponse> {
 25 | 	const methodLogger = Logger.forContext(
 26 | 		'controllers/atlassian.pullrequests.list.controller.ts',
 27 | 		'list',
 28 | 	);
 29 | 
 30 | 	try {
 31 | 		// Create defaults object with proper typing
 32 | 		const defaults: Partial<ListPullRequestsToolArgsType> = {
 33 | 			limit: DEFAULT_PAGE_SIZE,
 34 | 		};
 35 | 
 36 | 		// Apply defaults
 37 | 		const mergedOptions = applyDefaults<ListPullRequestsToolArgsType>(
 38 | 			options,
 39 | 			defaults,
 40 | 		);
 41 | 
 42 | 		// Handle optional workspaceSlug - get default if not provided
 43 | 		if (!mergedOptions.workspaceSlug) {
 44 | 			methodLogger.debug(
 45 | 				'No workspace provided, fetching default workspace',
 46 | 			);
 47 | 			const defaultWorkspace = await getDefaultWorkspace();
 48 | 			if (!defaultWorkspace) {
 49 | 				throw new Error(
 50 | 					'Could not determine a default workspace. Please provide a workspaceSlug.',
 51 | 				);
 52 | 			}
 53 | 			mergedOptions.workspaceSlug = defaultWorkspace;
 54 | 			methodLogger.debug(
 55 | 				`Using default workspace: ${mergedOptions.workspaceSlug}`,
 56 | 			);
 57 | 		}
 58 | 
 59 | 		const { workspaceSlug, repoSlug } = mergedOptions;
 60 | 
 61 | 		if (!workspaceSlug || !repoSlug) {
 62 | 			throw new Error('Workspace slug and repository slug are required');
 63 | 		}
 64 | 
 65 | 		methodLogger.debug(
 66 | 			`Listing pull requests for ${workspaceSlug}/${repoSlug}...`,
 67 | 			mergedOptions,
 68 | 		);
 69 | 
 70 | 		// Format the query for Bitbucket API if provided - specifically target title/description
 71 | 		const formattedQuery = mergedOptions.query
 72 | 			? `(title ~ "${mergedOptions.query}" OR description ~ "${mergedOptions.query}")` // Construct specific query for PRs
 73 | 			: undefined;
 74 | 
 75 | 		// Map controller options to service parameters
 76 | 		const serviceParams: ListPullRequestsParams = {
 77 | 			workspace: workspaceSlug,
 78 | 			repo_slug: repoSlug,
 79 | 			pagelen: mergedOptions.limit,
 80 | 			page: mergedOptions.cursor
 81 | 				? parseInt(mergedOptions.cursor, 10)
 82 | 				: undefined,
 83 | 			state: mergedOptions.state,
 84 | 			sort: '-updated_on', // Sort by most recently updated first
 85 | 			...(formattedQuery && { q: formattedQuery }),
 86 | 		};
 87 | 
 88 | 		methodLogger.debug('Using service parameters:', serviceParams);
 89 | 
 90 | 		const pullRequestsData =
 91 | 			await atlassianPullRequestsService.list(serviceParams);
 92 | 
 93 | 		methodLogger.debug(
 94 | 			`Retrieved ${pullRequestsData.values?.length || 0} pull requests`,
 95 | 		);
 96 | 
 97 | 		// Extract pagination information using the utility
 98 | 		const pagination = extractPaginationInfo(
 99 | 			pullRequestsData,
100 | 			PaginationType.PAGE,
101 | 		);
102 | 
103 | 		// Format the pull requests data for display using the formatter
104 | 		const formattedPullRequests = formatPullRequestsList(pullRequestsData);
105 | 
106 | 		// Create the final content by combining the formatted pull requests with pagination information
107 | 		let finalContent = formattedPullRequests;
108 | 
109 | 		// Add pagination information if available
110 | 		if (
111 | 			pagination &&
112 | 			(pagination.hasMore || pagination.count !== undefined)
113 | 		) {
114 | 			const paginationString = formatPagination(pagination);
115 | 			finalContent += '\n\n' + paginationString;
116 | 		}
117 | 
118 | 		return {
119 | 			content: finalContent,
120 | 		};
121 | 	} catch (error) {
122 | 		// Use the standardized error handler
123 | 		throw handleControllerError(error, {
124 | 			entityType: 'Pull Requests',
125 | 			operation: 'listing',
126 | 			source: 'controllers/atlassian.pullrequests.list.controller.ts@list',
127 | 			additionalInfo: { options },
128 | 		});
129 | 	}
130 | }
131 | 
132 | // Export the controller functions
133 | export default { list };
134 | 
```

--------------------------------------------------------------------------------
/src/services/vendor.atlassian.repositories.diff.service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | import { Logger } from '../utils/logger.util.js';
  3 | import { NETWORK_TIMEOUTS } from '../utils/constants.util.js';
  4 | import {
  5 | 	fetchAtlassian,
  6 | 	getAtlassianCredentials,
  7 | } from '../utils/transport.util.js';
  8 | import {
  9 | 	createApiError,
 10 | 	createAuthMissingError,
 11 | 	McpError,
 12 | } from '../utils/error.util.js';
 13 | import {
 14 | 	GetDiffstatParamsSchema,
 15 | 	DiffstatResponseSchema,
 16 | 	GetRawDiffParamsSchema,
 17 | 	type GetDiffstatParams,
 18 | 	type DiffstatResponse,
 19 | 	type GetRawDiffParams,
 20 | } from './vendor.atlassian.repositories.diff.types.js';
 21 | 
 22 | /**
 23 |  * Base API path for Bitbucket REST API v2
 24 |  */
 25 | const API_PATH = '/2.0';
 26 | 
 27 | const serviceLogger = Logger.forContext(
 28 | 	'services/vendor.atlassian.repositories.diff.service.ts',
 29 | );
 30 | serviceLogger.debug('Bitbucket diff service initialised');
 31 | 
 32 | /**
 33 |  * Retrieve diffstat (per–file summary) between two refs (branches, tags or commits).
 34 |  * Follows Bitbucket Cloud endpoint:
 35 |  *   GET /2.0/repositories/{workspace}/{repo_slug}/diffstat/{spec}
 36 |  */
 37 | export async function getDiffstat(
 38 | 	params: GetDiffstatParams,
 39 | ): Promise<DiffstatResponse> {
 40 | 	const methodLogger = serviceLogger.forMethod('getDiffstat');
 41 | 	methodLogger.debug('Fetching diffstat with params', params);
 42 | 
 43 | 	// Validate params
 44 | 	try {
 45 | 		GetDiffstatParamsSchema.parse(params);
 46 | 	} catch (err) {
 47 | 		if (err instanceof z.ZodError) {
 48 | 			throw createApiError(
 49 | 				`Invalid parameters: ${err.issues.map((e) => e.message).join(', ')}`,
 50 | 				400,
 51 | 				err,
 52 | 			);
 53 | 		}
 54 | 		throw err;
 55 | 	}
 56 | 
 57 | 	const credentials = getAtlassianCredentials();
 58 | 	if (!credentials) {
 59 | 		throw createAuthMissingError('Atlassian credentials are required');
 60 | 	}
 61 | 
 62 | 	const query = new URLSearchParams();
 63 | 	if (params.pagelen) query.set('pagelen', String(params.pagelen));
 64 | 	if (params.cursor) query.set('page', String(params.cursor));
 65 | 	if (params.topic !== undefined) query.set('topic', String(params.topic));
 66 | 
 67 | 	const queryString = query.toString() ? `?${query.toString()}` : '';
 68 | 	const encodedSpec = encodeURIComponent(params.spec);
 69 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/diffstat/${encodedSpec}${queryString}`;
 70 | 
 71 | 	methodLogger.debug(`Requesting: ${path}`);
 72 | 	try {
 73 | 		const rawData = await fetchAtlassian(credentials, path);
 74 | 		try {
 75 | 			const validated = DiffstatResponseSchema.parse(rawData);
 76 | 			return validated;
 77 | 		} catch (error) {
 78 | 			if (error instanceof z.ZodError) {
 79 | 				methodLogger.error(
 80 | 					'Bitbucket API response validation failed:',
 81 | 					error.format(),
 82 | 				);
 83 | 				throw createApiError(
 84 | 					`Invalid response format from Bitbucket API for diffstat: ${error.message}`,
 85 | 					500,
 86 | 					error,
 87 | 				);
 88 | 			}
 89 | 			throw error; // Re-throw any other errors
 90 | 		}
 91 | 	} catch (error) {
 92 | 		if (error instanceof McpError) throw error;
 93 | 		throw createApiError(
 94 | 			`Failed to fetch diffstat: ${error instanceof Error ? error.message : String(error)}`,
 95 | 			500,
 96 | 			error,
 97 | 		);
 98 | 	}
 99 | }
100 | 
101 | /**
102 |  * Retrieve raw unified diff between two refs.
103 |  * Endpoint: /diff/{spec}
104 |  */
105 | export async function getRawDiff(params: GetRawDiffParams): Promise<string> {
106 | 	const methodLogger = serviceLogger.forMethod('getRawDiff');
107 | 	methodLogger.debug('Fetching raw diff', params);
108 | 	try {
109 | 		GetRawDiffParamsSchema.parse(params);
110 | 	} catch (err) {
111 | 		if (err instanceof z.ZodError) {
112 | 			throw createApiError(
113 | 				`Invalid parameters: ${err.issues.map((e) => e.message).join(', ')}`,
114 | 				400,
115 | 				err,
116 | 			);
117 | 		}
118 | 		throw err;
119 | 	}
120 | 	const credentials = getAtlassianCredentials();
121 | 	if (!credentials) {
122 | 		throw createAuthMissingError('Atlassian credentials are required');
123 | 	}
124 | 
125 | 	const encodedSpec = encodeURIComponent(params.spec);
126 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/diff/${encodedSpec}`;
127 | 	methodLogger.debug(`Requesting: ${path}`);
128 | 	try {
129 | 		// fetchAtlassian will return string for text/plain
130 | 		const diffText = await fetchAtlassian<string>(credentials, path, {
131 | 			timeout: NETWORK_TIMEOUTS.LARGE_REQUEST_TIMEOUT,
132 | 		});
133 | 		return diffText;
134 | 	} catch (error) {
135 | 		if (error instanceof McpError) throw error;
136 | 		throw createApiError(
137 | 			`Failed to fetch raw diff: ${error instanceof Error ? error.message : String(error)}`,
138 | 			500,
139 | 			error,
140 | 		);
141 | 	}
142 | }
143 | 
```
Page 1/6FirstPrevNextLast