#
tokens: 49779/50000 71/114 files (page 1/4)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 4. Use http://codebase.md/aashari/mcp-server-atlassian-bitbucket?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:
--------------------------------------------------------------------------------

```

```

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

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

```

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

```
{
  "singleQuote": true,
  "semi": true,
  "useTabs": true,
  "tabWidth": 4,
  "printWidth": 80,
  "trailingComma": "all"
} 
```

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

```
# This file is for local development only
# The CI/CD workflow will create its own .npmrc files

# For npm registry
registry=https://registry.npmjs.org/

# GitHub Packages configuration removed

```

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

```
# Source code
src/
*.ts
!*.d.ts

# Tests
*.test.ts
*.test.js
__tests__/
coverage/
jest.config.js

# Development files
.github/
.git/
.gitignore
.eslintrc
.eslintrc.js
.eslintignore
.prettierrc
.prettierrc.js
tsconfig.json
*.tsbuildinfo

# Editor directories
.idea/
.vscode/
*.swp
*.swo

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# CI/CD
.travis.yml

# Runtime data
.env
.env.* 
```

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

```
# Dependency directories
node_modules/
.npm

# TypeScript output
dist/
build/
*.tsbuildinfo

# Coverage directories
coverage/
.nyc_output/

# Environment variables
.env
.env.local
.env.*.local

# Log files
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# IDE files
.idea/
.vscode/
*.sublime-project
*.sublime-workspace
.project
.classpath
.settings/
.DS_Store

# Temp directories
.tmp/
temp/

# Backup files
*.bak

# Editor directories and files
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# macOS
.DS_Store

# Misc
.yarn-integrity

```

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

```
# Enable debug logging
DEBUG=false

# Atlassian Configuration - Method 1 (Standard Atlassian - recommended)
# Use this for general Atlassian services (works with Bitbucket, Jira, Confluence)
ATLASSIAN_SITE_NAME=your-instance
[email protected]
ATLASSIAN_API_TOKEN=

# Atlassian Configuration - Method 2 (Bitbucket-specific alternative)
# Use this if you prefer Bitbucket username + app password authentication
# ATLASSIAN_BITBUCKET_USERNAME=your-bitbucket-username
# ATLASSIAN_BITBUCKET_APP_PASSWORD=your-app-password

# Optional: Default workspace for commands
# BITBUCKET_DEFAULT_WORKSPACE=your-main-workspace-slug

```

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

```json
{
	"branches": ["main"],
	"plugins": [
		"@semantic-release/commit-analyzer",
		"@semantic-release/release-notes-generator",
		"@semantic-release/changelog",
		[
			"@semantic-release/exec",
			{
				"prepareCmd": "node scripts/update-version.js ${nextRelease.version} && npm run build && chmod +x dist/index.js"
			}
		],
		[
			"@semantic-release/npm",
			{
				"npmPublish": true,
				"pkgRoot": "."
			}
		],
		[
			"@semantic-release/git",
			{
				"assets": [
					"package.json",
					"CHANGELOG.md",
					"src/index.ts",
					"src/cli/index.ts"
				],
				"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
			}
		],
		"@semantic-release/github"
	]
}

```

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

```markdown
# Connect AI to Your Bitbucket Repositories

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.

[![NPM Version](https://img.shields.io/npm/v/@aashari/mcp-server-atlassian-bitbucket)](https://www.npmjs.com/package/@aashari/mcp-server-atlassian-bitbucket)

## What You Can Do

✅ **Ask AI about your code**: "What's the latest commit in my main repository?"  
✅ **Get PR insights**: "Show me all open pull requests that need review"  
✅ **Search your codebase**: "Find all JavaScript files that use the authentication function"  
✅ **Review code changes**: "Compare the differences between my feature branch and main"  
✅ **Manage pull requests**: "Create a PR for my new-feature branch"  
✅ **Automate workflows**: "Add a comment to PR #123 with the test results"  

## Perfect For

- **Developers** who want AI assistance with code reviews and repository management
- **Team Leads** needing quick insights into project status and pull request activity  
- **DevOps Engineers** automating repository workflows and branch management
- **Anyone** who wants to interact with Bitbucket using natural language

## Quick Start

Get up and running in 2 minutes:

### 1. Get Your Bitbucket Credentials

> ⚠️ **IMPORTANT**: Bitbucket App Passwords are being deprecated and will be removed by **June 2026**. We recommend using **Scoped API Tokens** for new setups.

#### Option A: Scoped API Token (Recommended - Future-Proof)

**Bitbucket is deprecating app passwords**. Use the new scoped API tokens instead:

1. Go to [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
2. Click **"Create API token with scopes"**
3. Select **"Bitbucket"** as the product
4. Choose the appropriate scopes:
   - **For read-only access**: `repository`, `workspace`
   - **For full functionality**: `repository`, `workspace`, `pullrequest`
5. Copy the generated token (starts with `ATATT`)
6. Use with your Atlassian email as the username

#### Option B: App Password (Legacy - Will be deprecated)

Generate a Bitbucket App Password (legacy method):
1. Go to [Bitbucket App Passwords](https://bitbucket.org/account/settings/app-passwords/)
2. Click "Create app password"
3. Give it a name like "AI Assistant"
4. Select these permissions:
   - **Workspaces**: Read
   - **Repositories**: Read (and Write if you want AI to create PRs/comments)
   - **Pull Requests**: Read (and Write for PR management)

### 2. Try It Instantly

```bash
# Set your credentials (choose one method)

# Method 1: Scoped API Token (recommended - future-proof)
export ATLASSIAN_USER_EMAIL="[email protected]"
export ATLASSIAN_API_TOKEN="your_scoped_api_token"  # Token starting with ATATT

# OR Method 2: Legacy App Password (will be deprecated June 2026)
export ATLASSIAN_BITBUCKET_USERNAME="your_username"
export ATLASSIAN_BITBUCKET_APP_PASSWORD="your_app_password"

# List your workspaces
npx -y @aashari/mcp-server-atlassian-bitbucket ls-workspaces

# List repositories in your workspace
npx -y @aashari/mcp-server-atlassian-bitbucket ls-repos --workspace-slug your-workspace

# Get details about a specific repository  
npx -y @aashari/mcp-server-atlassian-bitbucket get-repo --workspace-slug your-workspace --repo-slug your-repo
```

## Connect to AI Assistants

### For Claude Desktop Users

Add this to your Claude configuration file (`~/.claude/claude_desktop_config.json`):

**Option 1: Scoped API Token (recommended - future-proof)**
```json
{
  "mcpServers": {
    "bitbucket": {
      "command": "npx",
      "args": ["-y", "@aashari/mcp-server-atlassian-bitbucket"],
      "env": {
        "ATLASSIAN_USER_EMAIL": "[email protected]",
        "ATLASSIAN_API_TOKEN": "your_scoped_api_token"
      }
    }
  }
}
```

**Option 2: Legacy App Password (will be deprecated June 2026)**
```json
{
  "mcpServers": {
    "bitbucket": {
      "command": "npx",
      "args": ["-y", "@aashari/mcp-server-atlassian-bitbucket"],
      "env": {
        "ATLASSIAN_BITBUCKET_USERNAME": "your_username",
        "ATLASSIAN_BITBUCKET_APP_PASSWORD": "your_app_password"
      }
    }
  }
}
```

Restart Claude Desktop, and you'll see "🔗 bitbucket" in the status bar.

### For Other AI Assistants

Most AI assistants support MCP. Install the server globally:

```bash
npm install -g @aashari/mcp-server-atlassian-bitbucket
```

Then configure your AI assistant to use the MCP server with STDIO transport.

### Alternative: Configuration File

Create `~/.mcp/configs.json` for system-wide configuration:

**Option 1: Scoped API Token (recommended - future-proof)**
```json
{
  "bitbucket": {
    "environments": {
      "ATLASSIAN_USER_EMAIL": "[email protected]",
      "ATLASSIAN_API_TOKEN": "your_scoped_api_token",
      "BITBUCKET_DEFAULT_WORKSPACE": "your_main_workspace"
    }
  }
}
```

**Option 2: Legacy App Password (will be deprecated June 2026)**
```json
{
  "bitbucket": {
    "environments": {
      "ATLASSIAN_BITBUCKET_USERNAME": "your_username",
      "ATLASSIAN_BITBUCKET_APP_PASSWORD": "your_app_password",
      "BITBUCKET_DEFAULT_WORKSPACE": "your_main_workspace"
    }
  }
}
```

**Alternative config keys:** The system also accepts `"atlassian-bitbucket"`, `"@aashari/mcp-server-atlassian-bitbucket"`, or `"mcp-server-atlassian-bitbucket"` instead of `"bitbucket"`.

## Real-World Examples

### 🔍 Explore Your Repositories

Ask your AI assistant:
- *"List all repositories in my main workspace"*
- *"Show me details about the backend-api repository"*
- *"What's the commit history for the feature-auth branch?"*
- *"Get the content of src/config.js from the main branch"*

### 📋 Manage Pull Requests

Ask your AI assistant:
- *"Show me all open pull requests that need review"*
- *"Get details about pull request #42 including the code changes"*
- *"Create a pull request from feature-login to main branch"*
- *"Add a comment to PR #15 saying the tests passed"*
- *"Approve pull request #33"*

### 🔧 Work with Branches and Code

Ask your AI assistant:
- *"Compare my feature branch with the main branch"*
- *"Create a new branch called hotfix-login from the main branch"*
- *"List all branches in the user-service repository"*
- *"Show me the differences between commits abc123 and def456"*

### 🔎 Search and Discovery

Ask your AI assistant:
- *"Search for JavaScript files that contain 'authentication'"*
- *"Find all pull requests related to the login feature"*
- *"Search for repositories in the mobile project"*
- *"Show me code files that use the React framework"*

## Troubleshooting

### "Authentication failed" or "403 Forbidden"

1. **Choose the right authentication method**:
   - **Standard Atlassian method**: Use your Atlassian account email + API token (works with any Atlassian service)
   - **Bitbucket-specific method**: Use your Bitbucket username + App password (Bitbucket only)

2. **For Bitbucket App Passwords** (if using Option 2):
   - Go to [Bitbucket App Passwords](https://bitbucket.org/account/settings/app-passwords/)
   - Make sure your app password has the right permissions (Workspaces: Read, Repositories: Read, Pull Requests: Read)

3. **For Scoped API Tokens** (recommended):
   - Go to [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
   - Make sure your token is still active and has the right scopes
   - Update your `~/.mcp/configs.json` file to use the new scoped API token format:
   ```json
   {
     "@aashari/mcp-server-atlassian-bitbucket": {
       "environments": {
         "ATLASSIAN_USER_EMAIL": "[email protected]",
         "ATLASSIAN_API_TOKEN": "ATATT3xFfGF0..."
       }
     }
   }
   ```

4. **Verify your credentials**:
   ```bash
   # Test your credentials work
   npx -y @aashari/mcp-server-atlassian-bitbucket ls-workspaces
   ```

### "Workspace not found" or "Repository not found"

1. **Check your workspace slug**:
   ```bash
   # List your workspaces to see the correct slugs
   npx -y @aashari/mcp-server-atlassian-bitbucket ls-workspaces
   ```

2. **Use the exact slug from Bitbucket URL**:
   - If your repo URL is `https://bitbucket.org/myteam/my-repo`
   - Workspace slug is `myteam`
   - Repository slug is `my-repo`

### "No default workspace configured"

Set a default workspace to avoid specifying it every time:
```bash
export BITBUCKET_DEFAULT_WORKSPACE="your-main-workspace-slug"
```

### Claude Desktop Integration Issues

1. **Restart Claude Desktop** after updating the config file
2. **Check the status bar** for the "🔗 bitbucket" indicator
3. **Verify config file location**:
   - macOS: `~/.claude/claude_desktop_config.json`
   - Windows: `%APPDATA%\Claude\claude_desktop_config.json`

### Getting Help

If you're still having issues:
1. Run a simple test command to verify everything works
2. Check the [GitHub Issues](https://github.com/aashari/mcp-server-atlassian-bitbucket/issues) for similar problems
3. Create a new issue with your error message and setup details

## Frequently Asked Questions

### What permissions do I need?

**For Scoped API Tokens** (recommended):
- Your regular Atlassian account with access to Bitbucket
- Scoped API token created at [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
- Required scopes: `repository`, `workspace` (add `pullrequest` for PR management)

**For Bitbucket App Passwords** (legacy - being deprecated):
- For **read-only access** (viewing repos, PRs, commits):
  - Workspaces: Read
  - Repositories: Read  
  - Pull Requests: Read
- For **full functionality** (creating PRs, commenting):
  - Add "Write" permissions for Repositories and Pull Requests

### Can I use this with private repositories?

Yes! This works with both public and private repositories. You just need the appropriate permissions through your Bitbucket App Password.

### Do I need to specify workspace every time?

No! Set `BITBUCKET_DEFAULT_WORKSPACE` in your environment or config file, and it will be used automatically when you don't specify one.

### What AI assistants does this work with?

Any AI assistant that supports the Model Context Protocol (MCP):
- Claude Desktop (most popular)
- Cursor AI
- Continue.dev
- Many others

### Is my data secure?

Yes! This tool:
- Runs entirely on your local machine
- Uses your own Bitbucket credentials
- Never sends your data to third parties
- Only accesses what you give it permission to access

### Can I use this for multiple Bitbucket accounts?

Currently, each installation supports one set of credentials. For multiple accounts, you'd need separate configurations.

## Support

Need help? Here's how to get assistance:

1. **Check the troubleshooting section above** - most common issues are covered there
2. **Visit our GitHub repository** for documentation and examples: [github.com/aashari/mcp-server-atlassian-bitbucket](https://github.com/aashari/mcp-server-atlassian-bitbucket)
3. **Report issues** at [GitHub Issues](https://github.com/aashari/mcp-server-atlassian-bitbucket/issues)
4. **Start a discussion** for feature requests or general questions

---

*Made with ❤️ for developers who want to bring AI into their Bitbucket workflow.*

```

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

```json
{
  "type": "module"
} 
```

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

```yaml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    versioning-strategy: auto
    labels:
      - "dependencies"
    commit-message:
      prefix: "chore"
      include: "scope"
    allow:
      - dependency-type: "direct"
    ignore:
      - dependency-name: "*"
        update-types: ["version-update:semver-patch"]
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5
    labels:
      - "dependencies"
      - "github-actions" 
```

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

```typescript
/**
 * Types of content that can be searched in Bitbucket
 */
export enum ContentType {
	WIKI = 'wiki',
	ISSUE = 'issue',
	PULLREQUEST = 'pullrequest',
	COMMIT = 'commit',
	BRANCH = 'branch',
	TAG = 'tag',
}

/**
 * Get the display name for a content type
 */
export function getContentTypeDisplay(type: ContentType): string {
	switch (type) {
		case ContentType.WIKI:
			return 'Wiki';
		case ContentType.ISSUE:
			return 'Issue';
		case ContentType.PULLREQUEST:
			return 'Pull Request';
		case ContentType.COMMIT:
			return 'Commit';
		case ContentType.BRANCH:
			return 'Branch';
		case ContentType.TAG:
			return 'Tag';
		default:
			return type;
	}
}

```

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

```javascript
// Jest setup file to suppress console warnings during tests
// This improves test output readability while maintaining error visibility

const originalConsoleWarn = console.warn;
const originalConsoleInfo = console.info;
const originalConsoleDebug = console.debug;

beforeAll(() => {
  // Suppress console.warn, console.info, and console.debug during tests
  // while keeping console.error for actual issues
  console.warn = jest.fn();
  console.info = jest.fn();
  console.debug = jest.fn();
});

afterAll(() => {
  // Restore original console methods
  console.warn = originalConsoleWarn;
  console.info = originalConsoleInfo;
  console.debug = originalConsoleDebug;
});
```

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

```yaml
name: CI - Dependency Check

on:
    schedule:
        - cron: '0 5 * * 1' # Run at 5 AM UTC every Monday
    workflow_dispatch: # Allow manual triggering

jobs:
    validate:
        runs-on: ubuntu-latest
        steps:
            - name: Checkout code
              uses: actions/checkout@v5

            - name: Setup Node.js
              uses: actions/setup-node@v5
              with:
                  node-version: '22'
                  cache: 'npm'

            - name: Install dependencies
              run: npm ci

            - name: Run npm audit
              run: npm audit

            - name: Check for outdated dependencies
              run: npm outdated || true

            - name: Run tests
              run: npm test

            - name: Run linting
              run: npm run lint

            - name: Build project
              run: npm run build

```

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

```typescript
/**
 * Default values for pagination across the application.
 * These values should be used consistently throughout the codebase.
 */

/**
 * Default page size for all list operations.
 * This value determines how many items are returned in a single page by default.
 */
export const DEFAULT_PAGE_SIZE = 25;

/**
 * Apply default values to options object.
 * This utility ensures that default values are consistently applied.
 *
 * @param options Options object that may have some values undefined
 * @param defaults Default values to apply when options values are undefined
 * @returns Options object with default values applied
 *
 * @example
 * const options = applyDefaults({ limit: 10 }, { limit: DEFAULT_PAGE_SIZE, includeBranches: true });
 * // Result: { limit: 10, includeBranches: true }
 */
export function applyDefaults<T extends object>(
	options: Partial<T>,
	defaults: Partial<T>,
): T {
	return {
		...defaults,
		...Object.fromEntries(
			Object.entries(options).filter(([_, value]) => value !== undefined),
		),
	} as T;
}

```

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

```
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettierPlugin from 'eslint-plugin-prettier';
import eslintConfigPrettier from 'eslint-config-prettier';

export default tseslint.config(
	{
		ignores: ['node_modules/**', 'dist/**', 'examples/**'],
	},
	eslint.configs.recommended,
	...tseslint.configs.recommended,
	{
		plugins: {
			prettier: prettierPlugin,
		},
		rules: {
			'prettier/prettier': 'error',
			indent: ['error', 'tab', { SwitchCase: 1 }],
			'@typescript-eslint/no-unused-vars': [
				'error',
				{ argsIgnorePattern: '^_' },
			],
		},
		languageOptions: {
			parserOptions: {
				ecmaVersion: 'latest',
				sourceType: 'module',
			},
			globals: {
				node: 'readonly',
				jest: 'readonly',
			},
		},
	},
	// Special rules for test files
	{
		files: ['**/*.test.ts'],
		rules: {
			'@typescript-eslint/no-explicit-any': 'off',
			'@typescript-eslint/no-require-imports': 'off',
			'@typescript-eslint/no-unsafe-function-type': 'off',
			'@typescript-eslint/no-unused-vars': 'off',
		},
	},
	eslintConfigPrettier,
);

```

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

```yaml
name: CI - Dependabot Auto-merge

on:
    pull_request:
        branches: [main]

permissions:
    contents: write
    pull-requests: write
    checks: read

jobs:
    auto-merge-dependabot:
        runs-on: ubuntu-latest
        if: github.actor == 'dependabot[bot]'
        steps:
            - name: Checkout code
              uses: actions/checkout@v5

            - name: Setup Node.js
              uses: actions/setup-node@v5
              with:
                  node-version: '22'
                  cache: 'npm'

            - name: Install dependencies
              run: npm ci

            - name: Run tests
              run: npm test

            - name: Run linting
              run: npm run lint

            - name: Auto-approve PR
              uses: hmarr/auto-approve-action@v4
              with:
                  github-token: ${{ secrets.GITHUB_TOKEN }}

            - name: Enable auto-merge
              if: success()
              run: gh pr merge --auto --merge "$PR_URL"
              env:
                  PR_URL: ${{ github.event.pull_request.html_url }}
                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

```

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

```typescript
import { z } from 'zod';

/**
 * Base pagination arguments for all tools
 */
const PaginationArgs = {
	limit: z
		.number()
		.int()
		.positive()
		.max(100)
		.optional()
		.describe(
			'Maximum number of items to return (1-100). Controls the response size. Defaults to 25 if omitted.',
		),

	cursor: z
		.string()
		.optional()
		.describe(
			'Pagination cursor for retrieving the next set of results. Obtained from previous response when more results are available.',
		),
};

/**
 * Schema for list-workspaces tool arguments
 */
export const ListWorkspacesToolArgs = z.object({
	/**
	 * Maximum number of workspaces to return and pagination
	 */
	...PaginationArgs,
});

export type ListWorkspacesToolArgsType = z.infer<typeof ListWorkspacesToolArgs>;

/**
 * Schema for get-workspace tool arguments
 */
export const GetWorkspaceToolArgs = z.object({
	/**
	 * Workspace slug to retrieve
	 */
	workspaceSlug: z
		.string()
		.min(1, 'Workspace slug is required')
		.describe(
			'Workspace slug to retrieve detailed information for. Must be a valid workspace slug from your Bitbucket account. Example: "myteam"',
		),
});

export type GetWorkspaceToolArgsType = z.infer<typeof GetWorkspaceToolArgs>;

```

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

```typescript
import { ContentType } from '../utils/atlassian.util.js';

/**
 * Content search parameters
 */
export interface ContentSearchParams {
	/** Workspace slug to search in */
	workspaceSlug: string;
	/** Query string to search for */
	query: string;
	/** Maximum number of results to return (default: 25) */
	limit?: number;
	/** Page number for pagination (default: 1) */
	page?: number;
	/** Repository slug to search in (optional) */
	repoSlug?: string;
	/** Type of content to search for (optional) */
	contentType?: ContentType;
}

/**
 * Generic content search result item
 */
export interface ContentSearchResultItem {
	// Most Bitbucket content items will have these fields
	type?: string;
	title?: string;
	name?: string;
	summary?: string;
	description?: string;
	content?: string;
	created_on?: string;
	updated_on?: string;
	links?: {
		self?: { href: string };
		html?: { href: string };
		[key: string]: unknown;
	};
	// Allow additional properties as Bitbucket returns different fields per content type
	[key: string]: unknown;
}

/**
 * Content search response
 */
export interface ContentSearchResponse {
	size: number;
	page: number;
	pagelen: number;
	values: ContentSearchResultItem[];
	next?: string;
	previous?: string;
}

```

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

```typescript
/**
 * Application constants
 *
 * This file contains constants used throughout the application.
 * Centralizing these values makes them easier to maintain and update.
 */

/**
 * Current application version
 * This should match the version in package.json
 */
export const VERSION = '1.23.6';

/**
 * Package name with scope
 * Used for initialization and identification
 */
export const PACKAGE_NAME = '@aashari/mcp-server-atlassian-bitbucket';

/**
 * CLI command name
 * Used for binary name and CLI help text
 */
export const CLI_NAME = 'mcp-atlassian-bitbucket';

/**
 * Network timeout constants (in milliseconds)
 */
export const NETWORK_TIMEOUTS = {
	/** Default timeout for API requests (30 seconds) */
	DEFAULT_REQUEST_TIMEOUT: 30 * 1000,

	/** Timeout for large file operations like diffs (60 seconds) */
	LARGE_REQUEST_TIMEOUT: 60 * 1000,

	/** Timeout for search operations (45 seconds) */
	SEARCH_REQUEST_TIMEOUT: 45 * 1000,
} as const;

/**
 * Data limits to prevent excessive resource consumption (CWE-770)
 */
export const DATA_LIMITS = {
	/** Maximum response size in bytes (10MB) */
	MAX_RESPONSE_SIZE: 10 * 1024 * 1024,

	/** Maximum items per page for paginated requests */
	MAX_PAGE_SIZE: 100,

	/** Default page size when not specified */
	DEFAULT_PAGE_SIZE: 50,
} as const;

```

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

```javascript
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// Use dynamic import meta for ESM compatibility
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..');
const entryPoint = path.join(rootDir, 'dist', 'index.js');

try {
	if (fs.existsSync(entryPoint)) {
		// Ensure the file is executable (cross-platform)
		const currentMode = fs.statSync(entryPoint).mode;
		// Check if executable bits are set (user, group, or other)
		// Mode constants differ slightly across platforms, checking broadly
		const isExecutable =
			currentMode & fs.constants.S_IXUSR ||
			currentMode & fs.constants.S_IXGRP ||
			currentMode & fs.constants.S_IXOTH;

		if (!isExecutable) {
			// Set permissions to 755 (rwxr-xr-x) if not executable
			fs.chmodSync(entryPoint, 0o755);
			console.log(
				`Made ${path.relative(rootDir, entryPoint)} executable`,
			);
		} else {
			// console.log(`${path.relative(rootDir, entryPoint)} is already executable`);
		}
	} else {
		// console.warn(`${path.relative(rootDir, entryPoint)} not found, skipping chmod`);
	}
} catch (err) {
	// console.warn(`Failed to set executable permissions: ${err.message}`);
	// We use '|| true' in package.json, so no need to exit here
}

```

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

```typescript
import { z } from 'zod';

/**
 * Parameters for retrieving diffstat between two refs (branches, tags, or commit hashes)
 */
export const GetDiffstatParamsSchema = z.object({
	workspace: z.string().min(1, 'Workspace is required'),
	repo_slug: z.string().min(1, 'Repository slug is required'),
	/** e.g., "main..feature" or "hashA..hashB" */
	spec: z.string().min(1, 'Diff spec is required'),
	pagelen: z.number().int().positive().optional(),
	cursor: z.number().int().positive().optional(), // Bitbucket page-based cursor
	topic: z.boolean().optional(),
});

export type GetDiffstatParams = z.infer<typeof GetDiffstatParamsSchema>;

export const GetRawDiffParamsSchema = z.object({
	workspace: z.string().min(1),
	repo_slug: z.string().min(1),
	spec: z.string().min(1),
});

export type GetRawDiffParams = z.infer<typeof GetRawDiffParamsSchema>;

/**
 * Schema for a single file change entry in diffstat
 */
export const DiffstatFileChangeSchema = z.object({
	status: z.string(),
	old: z
		.object({
			path: z.string(),
			type: z.string().optional(),
		})
		.nullable()
		.optional(),
	new: z
		.object({
			path: z.string(),
			type: z.string().optional(),
		})
		.nullable()
		.optional(),
	lines_added: z.number().optional(),
	lines_removed: z.number().optional(),
});

/**
 * Schema for diffstat API response (paginated)
 */
export const DiffstatResponseSchema = z.object({
	pagelen: z.number().optional(),
	values: z.array(DiffstatFileChangeSchema),
	page: z.number().optional(),
	size: z.number().optional(),
	next: z.string().optional(),
	previous: z.string().optional(),
});

export type DiffstatResponse = z.infer<typeof DiffstatResponseSchema>;

```

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

```typescript
/**
 * Utilities for formatting and handling query parameters for the Bitbucket API.
 * These functions help convert user-friendly query strings into the format expected by Bitbucket's REST API.
 */

/**
 * Format a simple text query into Bitbucket's query syntax
 * Bitbucket API expects query parameters in a specific format for the 'q' parameter
 *
 * @param query - The search query string
 * @param field - Optional field to search in, defaults to 'name'
 * @returns Formatted query string for Bitbucket API
 *
 * @example
 * // Simple text search (returns: name ~ "vue3")
 * formatBitbucketQuery("vue3")
 *
 * @example
 * // Already formatted query (returns unchanged: name = "repository")
 * formatBitbucketQuery("name = \"repository\"")
 *
 * @example
 * // With specific field (returns: description ~ "API")
 * formatBitbucketQuery("API", "description")
 */
export function formatBitbucketQuery(
	query: string,
	field: string = 'name',
): string {
	// If the query is empty, return it as is
	if (!query || query.trim() === '') {
		return query;
	}

	// Regular expression to check if the query already contains operators
	// like ~, =, !=, >, <, etc., which would indicate it's already formatted
	const operatorPattern = /[~=!<>]/;

	// If the query already contains operators, assume it's properly formatted
	if (operatorPattern.test(query)) {
		return query;
	}

	// If query is quoted, assume it's an exact match
	if (query.startsWith('"') && query.endsWith('"')) {
		return `${field} ~ ${query}`;
	}

	// Format simple text as a field search with fuzzy match
	// Wrap in double quotes to handle spaces and special characters
	return `${field} ~ "${query}"`;
}

```

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

```typescript
/**
 * Common type definitions shared across controllers.
 * These types provide a standard interface for controller interactions.
 * Centralized here to ensure consistency across the codebase.
 */

/**
 * Common pagination information for API responses.
 * This is used for providing consistent pagination details internally.
 * Its formatted representation will be included directly in the content string.
 */
export interface ResponsePagination {
	/**
	 * Cursor for the next page of results, if available.
	 * This should be passed to subsequent requests to retrieve the next page.
	 */
	nextCursor?: string;

	/**
	 * Whether more results are available beyond the current page.
	 * When true, clients should use the nextCursor to retrieve more results.
	 */
	hasMore: boolean;

	/**
	 * The number of items in the current result set.
	 * This helps clients track how many items they've received.
	 */
	count?: number;

	/**
	 * The total number of items available across all pages, if known.
	 * Note: Not all APIs provide this. Check the specific API/tool documentation.
	 */
	total?: number;

	/**
	 * Page number for page-based pagination.
	 */
	page?: number;

	/**
	 * Page size for page-based pagination.
	 */
	size?: number;
}

/**
 * Common response structure for controller operations.
 * All controller methods should return this structure.
 */
export interface ControllerResponse {
	/**
	 * Formatted content to be displayed to the user.
	 * Contains a comprehensive Markdown-formatted string that includes all information:
	 * - Primary content (e.g., list items, details)
	 * - Any metadata (previously in metadata field)
	 * - Pagination information (previously in pagination field)
	 */
	content: string;
}

```

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

```typescript
import { Command } from 'commander';
import { Logger } from '../utils/logger.util.js';
import { VERSION, CLI_NAME } from '../utils/constants.util.js';

// Import Bitbucket-specific CLI modules
import atlassianWorkspacesCli from './atlassian.workspaces.cli.js';
import atlassianRepositoriesCli from './atlassian.repositories.cli.js';
import atlassianPullRequestsCli from './atlassian.pullrequests.cli.js';
import atlassianSearchCommands from './atlassian.search.cli.js';
import diffCli from './atlassian.diff.cli.js';

// Package description
const DESCRIPTION =
	'A Model Context Protocol (MCP) server for Atlassian Bitbucket integration';

// Create a contextualized logger for this file
const cliLogger = Logger.forContext('cli/index.ts');

// Log CLI initialization
cliLogger.debug('Bitbucket CLI module initialized');

export async function runCli(args: string[]) {
	const methodLogger = Logger.forContext('cli/index.ts', 'runCli');

	const program = new Command();

	program.name(CLI_NAME).description(DESCRIPTION).version(VERSION);

	// Register CLI commands
	atlassianWorkspacesCli.register(program);
	cliLogger.debug('Workspace commands registered');

	atlassianRepositoriesCli.register(program);
	cliLogger.debug('Repository commands registered');

	atlassianPullRequestsCli.register(program);
	cliLogger.debug('Pull Request commands registered');

	atlassianSearchCommands.register(program);
	cliLogger.debug('Search commands registered');

	diffCli.register(program);
	cliLogger.debug('Diff commands registered');

	// Handle unknown commands
	program.on('command:*', (operands) => {
		methodLogger.error(`Unknown command: ${operands[0]}`);
		console.log('');
		program.help();
		process.exit(1);
	});

	// Parse arguments; default to help if no command provided
	await program.parseAsync(args.length ? args : ['--help'], { from: 'user' });
}

```

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

```typescript
import { promisify } from 'util';
import { exec as callbackExec } from 'child_process';
import { Logger } from './logger.util.js';

const exec = promisify(callbackExec);
const utilLogger = Logger.forContext('utils/shell.util.ts');

/**
 * Executes a shell command.
 *
 * @param command The command string to execute.
 * @param operationDesc A brief description of the operation for logging purposes.
 * @returns A promise that resolves with the stdout of the command.
 * @throws An error if the command execution fails, including stderr.
 */
export async function executeShellCommand(
	command: string,
	operationDesc: string,
): Promise<string> {
	const methodLogger = utilLogger.forMethod('executeShellCommand');
	methodLogger.debug(`Attempting to ${operationDesc}: ${command}`);
	try {
		const { stdout, stderr } = await exec(command);
		if (stderr) {
			methodLogger.warn(`Stderr from ${operationDesc}: ${stderr}`);
			// Depending on the command, stderr might not always indicate a failure,
			// but for git clone, it usually does if stdout is empty.
			// If stdout is also present, it might be a warning.
		}
		methodLogger.info(
			`Successfully executed ${operationDesc}. Stdout: ${stdout}`,
		);
		return stdout || `Successfully ${operationDesc}.`; // Return stdout or a generic success message
	} catch (error: unknown) {
		methodLogger.error(`Failed to ${operationDesc}: ${command}`, error);

		let errorMessage = 'Unknown error during shell command execution.';
		if (error instanceof Error) {
			// Node's child_process.ExecException often has stdout and stderr properties
			const execError = error as Error & {
				stdout?: string;
				stderr?: string;
			};
			errorMessage =
				execError.stderr || execError.stdout || execError.message;
		} else if (typeof error === 'string') {
			errorMessage = error;
		}
		// Ensure a default message if somehow it's still undefined (though unlikely with above checks)
		if (!errorMessage && error) {
			errorMessage = String(error);
		}

		throw new Error(`Failed to ${operationDesc}: ${errorMessage}`);
	}
}

```

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

```typescript
import * as path from 'path';
import { Logger } from './logger.util.js';

const logger = Logger.forContext('utils/path.util.ts');

/**
 * Safely converts a path or path segments to a string, handling various input types.
 * Useful for ensuring consistent path string representations across different platforms.
 *
 * @param pathInput - The path or path segments to convert to a string
 * @returns The path as a normalized string
 */
export function pathToString(pathInput: string | string[] | unknown): string {
	if (Array.isArray(pathInput)) {
		return path.join(...pathInput);
	} else if (typeof pathInput === 'string') {
		return pathInput;
	} else if (pathInput instanceof URL) {
		return pathInput.pathname;
	} else if (
		pathInput &&
		typeof pathInput === 'object' &&
		'toString' in pathInput
	) {
		return String(pathInput);
	}

	logger.warn(`Unable to convert path input to string: ${typeof pathInput}`);
	return ''; // Return empty string for null/undefined
}

/**
 * Determines if a given path is within the user's home directory
 * which is generally considered a safe location for MCP operations.
 *
 * @param inputPath - Path to check
 * @returns True if the path is within the user's home directory
 */
export function isPathInHome(inputPath: string): boolean {
	const homePath = process.env.HOME || process.env.USERPROFILE || '';
	if (!homePath) {
		logger.warn('Could not determine user home directory');
		return false;
	}

	const resolvedPath = path.resolve(inputPath);
	return resolvedPath.startsWith(homePath);
}

/**
 * Gets a user-friendly display version of a path for use in messages.
 * For paths within the home directory, can replace with ~ for brevity.
 *
 * @param inputPath - Path to format
 * @param useHomeTilde - Whether to replace home directory with ~ symbol
 * @returns Formatted path string
 */
export function formatDisplayPath(
	inputPath: string,
	useHomeTilde = true,
): string {
	const homePath = process.env.HOME || process.env.USERPROFILE || '';

	if (
		useHomeTilde &&
		homePath &&
		path.resolve(inputPath).startsWith(homePath)
	) {
		return path.resolve(inputPath).replace(homePath, '~');
	}

	return path.resolve(inputPath);
}

```

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

```typescript
import { htmlToMarkdown } from './markdown.util.js';

describe('Markdown Utility', () => {
	describe('htmlToMarkdown', () => {
		it('should convert basic HTML to Markdown', () => {
			const html =
				'<h1>Hello World</h1><p>This is a <strong>test</strong>.</p>';
			const expected = '# Hello World\n\nThis is a **test**.';
			expect(htmlToMarkdown(html)).toBe(expected);
		});

		it('should handle empty input', () => {
			expect(htmlToMarkdown('')).toBe('');
			expect(htmlToMarkdown('   ')).toBe('');
		});

		it('should convert links correctly', () => {
			const html =
				'<p>Check out <a href="https://example.com">this link</a>.</p>';
			const expected = 'Check out [this link](https://example.com).';
			expect(htmlToMarkdown(html)).toBe(expected);
		});

		it('should convert lists correctly', () => {
			const html =
				'<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>';
			const expected = '-   Item 1\n-   Item 2\n-   Item 3';
			expect(htmlToMarkdown(html)).toBe(expected);
		});

		it('should convert tables correctly', () => {
			const html = `
                <table>
                    <thead>
                        <tr>
                            <th>Header 1</th>
                            <th>Header 2</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>Cell 1</td>
                            <td>Cell 2</td>
                        </tr>
                        <tr>
                            <td>Cell 3</td>
                            <td>Cell 4</td>
                        </tr>
                    </tbody>
                </table>
            `;
			const expected =
				'| Header 1 | Header 2 |\n| --- | --- |\n| Cell 1 | Cell 2 |\n| Cell 3 | Cell 4 |';

			// Normalize whitespace for comparison
			const normalizedResult = htmlToMarkdown(html)
				.replace(/\s+/g, ' ')
				.trim();
			const normalizedExpected = expected.replace(/\s+/g, ' ').trim();

			expect(normalizedResult).toBe(normalizedExpected);
		});

		it('should handle strikethrough text', () => {
			const html = '<p>This is <del>deleted</del> text.</p>';
			const expected = 'This is ~~deleted~~ text.';
			expect(htmlToMarkdown(html)).toBe(expected);
		});
	});
});

```

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

```typescript
import { Command } from 'commander';
import { Logger } from '../utils/logger.util.js';
import atlassianSearchController from '../controllers/atlassian.search.controller.js';
import { handleCliError } from '../utils/error-handler.util.js';
import { getDefaultWorkspace } from '../utils/workspace.util.js';

// Set up a logger for this module
const logger = Logger.forContext('cli/atlassian.search.cli.ts');

/**
 * Register the search commands with the CLI
 * @param program The commander program to register commands with
 */
function register(program: Command) {
	program
		.command('search')
		.description('Search Bitbucket for content matching a query')
		.requiredOption('-q, --query <query>', 'Search query')
		.option('-w, --workspace <workspace>', 'Workspace slug')
		.option('-r, --repo <repo>', 'Repository slug (required for PR search)')
		.option(
			'-t, --type <type>',
			'Search type (code, content, repositories, pullrequests)',
			'code',
		)
		.option(
			'-c, --content-type <contentType>',
			'Content type for content search (e.g., wiki, issue)',
		)
		.option(
			'-l, --language <language>',
			'Filter code search by programming language',
		)
		.option(
			'-e, --extension <extension>',
			'Filter code search by file extension',
		)
		.option('--limit <limit>', 'Maximum number of results to return', '20')
		.option('--cursor <cursor>', 'Pagination cursor')
		.action(async (options) => {
			const methodLogger = logger.forMethod('search');
			try {
				methodLogger.debug('CLI search command called with:', options);

				// Handle workspace
				let workspace = options.workspace;
				if (!workspace) {
					workspace = await getDefaultWorkspace();
					if (!workspace) {
						console.error(
							'Error: No workspace provided and no default workspace configured',
						);
						process.exit(1);
					}
					methodLogger.debug(`Using default workspace: ${workspace}`);
				}

				// Prepare controller options
				const controllerOptions = {
					workspace,
					repo: options.repo,
					query: options.query,
					type: options.type,
					contentType: options.contentType,
					language: options.language,
					extension: options.extension,
					limit: options.limit
						? parseInt(options.limit, 10)
						: undefined,
					cursor: options.cursor,
				};

				// Call the controller
				const result =
					await atlassianSearchController.search(controllerOptions);

				// Output the result
				console.log(result.content);
			} catch (error) {
				handleCliError(error);
			}
		});
}

export default { register };

```

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

```typescript
import { Logger } from '../utils/logger.util.js';
import { ControllerResponse } from '../types/common.types.js';
import { DEFAULT_PAGE_SIZE } from '../utils/defaults.util.js';
import atlassianPullRequestsService from '../services/vendor.atlassian.pullrequests.service.js';
import {
	extractPaginationInfo,
	PaginationType,
} from '../utils/pagination.util.js';
import { formatPagination } from '../utils/formatter.util.js';
import { formatPullRequestsList } from './atlassian.pullrequests.formatter.js';
import { ListPullRequestsParams } from '../services/vendor.atlassian.pullrequests.types.js';

/**
 * Handle search for pull requests (uses PR API with query filter)
 */
export async function handlePullRequestSearch(
	workspaceSlug: string,
	repoSlug?: string,
	query?: string,
	limit: number = DEFAULT_PAGE_SIZE,
	cursor?: string,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.search.pullrequests.controller.ts',
		'handlePullRequestSearch',
	);
	methodLogger.debug('Performing pull request search');

	if (!query) {
		return {
			content: 'Please provide a search query for pull request search.',
		};
	}

	try {
		// Format query for the Bitbucket API - specifically target title/description
		const formattedQuery = `(title ~ "${query}" OR description ~ "${query}")`;

		// Create the parameters for the PR service
		const params: ListPullRequestsParams = {
			workspace: workspaceSlug,
			repo_slug: repoSlug!, // Can safely use non-null assertion now that schema validation ensures it's present
			q: formattedQuery,
			pagelen: limit,
			page: cursor ? parseInt(cursor, 10) : undefined,
			sort: '-updated_on',
		};

		methodLogger.debug('Using PR search params:', params);

		const prData = await atlassianPullRequestsService.list(params);
		methodLogger.debug(
			`Search complete, found ${prData.values.length} matches`,
		);

		// Extract pagination information
		const pagination = extractPaginationInfo(prData, PaginationType.PAGE);

		// Format the search results
		const formattedPrs = formatPullRequestsList(prData);
		let finalContent = `# Pull Request Search Results\n\n${formattedPrs}`;

		// Add pagination information if available
		if (
			pagination &&
			(pagination.hasMore || pagination.count !== undefined)
		) {
			const paginationString = formatPagination(pagination);
			finalContent += '\n\n' + paginationString;
		}

		return {
			content: finalContent,
		};
	} catch (error) {
		methodLogger.error('Error performing pull request search:', error);
		throw error;
	}
}

```

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

```typescript
import { spawn } from 'child_process';
import { join } from 'path';

/**
 * Utility for testing CLI commands with real execution
 */
export class CliTestUtil {
	/**
	 * Executes a CLI command and returns the result
	 *
	 * @param args - CLI arguments to pass to the command
	 * @param options - Test options
	 * @returns Promise with stdout, stderr, and exit code
	 */
	static async runCommand(
		args: string[],
		options: {
			timeoutMs?: number;
			env?: Record<string, string>;
		} = {},
	): Promise<{
		stdout: string;
		stderr: string;
		exitCode: number;
	}> {
		// Default timeout of 30 seconds
		const timeoutMs = options.timeoutMs || 30000;

		// CLI execution path - points to the built CLI script
		const cliPath = join(process.cwd(), 'dist', 'index.js');

		return new Promise((resolve, reject) => {
			// Set up timeout handler
			const timeoutId = setTimeout(() => {
				child.kill();
				reject(new Error(`CLI command timed out after ${timeoutMs}ms`));
			}, timeoutMs);

			// Capture stdout and stderr
			let stdout = '';
			let stderr = '';

			// Spawn the process with given arguments
			const child = spawn('node', [cliPath, ...args], {
				env: {
					...process.env,
					...options.env,
				},
			});

			// Collect stdout data
			child.stdout.on('data', (data) => {
				stdout += data.toString();
			});

			// Collect stderr data
			child.stderr.on('data', (data) => {
				stderr += data.toString();
			});

			// Handle process completion
			child.on('close', (exitCode) => {
				clearTimeout(timeoutId);
				resolve({
					stdout,
					stderr,
					exitCode: exitCode ?? 0,
				});
			});

			// Handle process errors
			child.on('error', (err) => {
				clearTimeout(timeoutId);
				reject(err);
			});
		});
	}

	/**
	 * Validates that stdout contains expected strings/patterns
	 */
	static validateOutputContains(
		output: string,
		expectedPatterns: (string | RegExp)[],
	): void {
		for (const pattern of expectedPatterns) {
			if (typeof pattern === 'string') {
				expect(output).toContain(pattern);
			} else {
				expect(output).toMatch(pattern);
			}
		}
	}

	/**
	 * Validates Markdown output format
	 */
	static validateMarkdownOutput(output: string): void {
		// Check for Markdown heading
		expect(output).toMatch(/^#\s.+/m);

		// Check for markdown formatting elements like bold text, lists, etc.
		const markdownElements = [
			/\*\*.+\*\*/, // Bold text
			/-\s.+/, // List items
			/\|.+\|.+\|/, // Table rows
			/\[.+\]\(.+\)/, // Links
		];

		expect(markdownElements.some((pattern) => pattern.test(output))).toBe(
			true,
		);
	}
}

```

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

```typescript
import { Logger } from '../utils/logger.util.js';
import { ControllerResponse } from '../types/common.types.js';
import { DEFAULT_PAGE_SIZE } from '../utils/defaults.util.js';
import {
	extractPaginationInfo,
	PaginationType,
} from '../utils/pagination.util.js';
import { formatPagination } from '../utils/formatter.util.js';
import { formatContentSearchResults } from './atlassian.search.formatter.js';
import { ContentType } from '../utils/atlassian.util.js';
import { ContentSearchParams } from '../services/vendor.atlassian.search.types.js';
import atlassianSearchService from '../services/vendor.atlassian.search.service.js';

/**
 * Handle search for content (PRs, Issues, Wiki, etc.)
 */
export async function handleContentSearch(
	workspaceSlug: string,
	repoSlug?: string,
	query?: string,
	limit: number = DEFAULT_PAGE_SIZE,
	cursor?: string,
	contentType?: ContentType,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.search.content.controller.ts',
		'handleContentSearch',
	);
	methodLogger.debug('Performing content search');

	if (!query) {
		return {
			content: 'Please provide a search query for content search.',
		};
	}

	try {
		const params: ContentSearchParams = {
			workspaceSlug,
			query,
			limit,
			page: cursor ? parseInt(cursor, 10) : 1,
		};

		// Add optional parameters if provided
		if (repoSlug) {
			params.repoSlug = repoSlug;
		}

		if (contentType) {
			params.contentType = contentType;
		}

		methodLogger.debug('Content search params:', params);

		const searchResult = await atlassianSearchService.searchContent(params);

		methodLogger.debug(
			`Content search complete, found ${searchResult.size} matches`,
		);

		// Extract pagination information
		const pagination = extractPaginationInfo(
			{
				...searchResult,
				// For content search, the Bitbucket API returns values and size differently
				// We need to map it to a format that extractPaginationInfo can understand
				page: params.page,
				pagelen: limit,
			},
			PaginationType.PAGE,
		);

		// Format the search results
		const formattedResults = formatContentSearchResults(
			searchResult,
			contentType,
		);

		// Add pagination information if available
		let finalContent = formattedResults;
		if (
			pagination &&
			(pagination.hasMore || pagination.count !== undefined)
		) {
			const paginationString = formatPagination(pagination);
			finalContent += '\n\n' + paginationString;
		}

		return {
			content: finalContent,
		};
	} catch (searchError) {
		methodLogger.error('Error performing content search:', searchError);
		throw searchError;
	}
}

```

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

```typescript
/**
 * Markdown utility functions for converting HTML to Markdown
 * Uses Turndown library for HTML to Markdown conversion
 *
 * @see https://github.com/mixmark-io/turndown
 */

import TurndownService from 'turndown';
import { Logger } from './logger.util.js';

// Create a file-level logger for the module
const markdownLogger = Logger.forContext('utils/markdown.util.ts');

// DOM type definitions
interface HTMLElement {
	nodeName: string;
	parentNode?: Node;
	childNodes: NodeListOf<Node>;
}

interface Node {
	tagName?: string;
	childNodes: NodeListOf<Node>;
	parentNode?: Node;
}

interface NodeListOf<T> extends Array<T> {
	length: number;
}

// Create a singleton instance of TurndownService with default options
const turndownService = new TurndownService({
	headingStyle: 'atx', // Use # style headings
	bulletListMarker: '-', // Use - for bullet lists
	codeBlockStyle: 'fenced', // Use ``` for code blocks
	emDelimiter: '_', // Use _ for emphasis
	strongDelimiter: '**', // Use ** for strong
	linkStyle: 'inlined', // Use [text](url) for links
	linkReferenceStyle: 'full', // Use [text][id] + [id]: url for reference links
});

// Add custom rule for strikethrough
turndownService.addRule('strikethrough', {
	filter: (node: HTMLElement) => {
		return (
			node.nodeName.toLowerCase() === 'del' ||
			node.nodeName.toLowerCase() === 's' ||
			node.nodeName.toLowerCase() === 'strike'
		);
	},
	replacement: (content: string): string => `~~${content}~~`,
});

// Add custom rule for tables to improve table formatting
turndownService.addRule('tableCell', {
	filter: ['th', 'td'],
	replacement: (content: string, _node: TurndownService.Node): string => {
		return ` ${content} |`;
	},
});

turndownService.addRule('tableRow', {
	filter: 'tr',
	replacement: (content: string, node: TurndownService.Node): string => {
		let output = `|${content}\n`;

		// If this is the first row in a table head, add the header separator row
		if (
			node.parentNode &&
			'tagName' in node.parentNode &&
			node.parentNode.tagName === 'THEAD'
		) {
			const cellCount = node.childNodes.length;
			output += '|' + ' --- |'.repeat(cellCount) + '\n';
		}

		return output;
	},
});

/**
 * Convert HTML content to Markdown
 *
 * @param html - The HTML content to convert
 * @returns The converted Markdown content
 */
export function htmlToMarkdown(html: string): string {
	if (!html || html.trim() === '') {
		return '';
	}

	try {
		const markdown = turndownService.turndown(html);
		return markdown;
	} catch (error) {
		markdownLogger.error('Error converting HTML to Markdown:', error);
		// Return the original HTML if conversion fails
		return html;
	}
}

```

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

```yaml
name: CI - Semantic Release

# This workflow is triggered on every push to the main branch
# It analyzes commits and automatically releases a new version when needed
on:
    push:
        branches: [main]

jobs:
    release:
        name: Semantic Release
        runs-on: ubuntu-latest
        # Permissions needed for creating releases, updating issues, and publishing packages
        permissions:
            contents: write # Needed to create releases and tags
            issues: write # Needed to comment on issues
            pull-requests: write # Needed to comment on pull requests
            # packages permission removed as we're not using GitHub Packages
        steps:
            # Step 1: Check out the full Git history for proper versioning
            - name: Checkout
              uses: actions/checkout@v5
              with:
                  fetch-depth: 0 # Fetches all history for all branches and tags

            # Step 2: Setup Node.js environment
            - name: Setup Node.js
              uses: actions/setup-node@v5
              with:
                  node-version: 22 # Using Node.js 22
                  cache: 'npm' # Enable npm caching

            # Step 3: Install dependencies with clean install
            - name: Install dependencies
              run: npm ci # Clean install preserving package-lock.json

            # Step 4: Build the package
            - name: Build
              run: npm run build # Compiles TypeScript to JavaScript

            # Step 5: Ensure executable permissions
            - name: Set executable permissions
              run: chmod +x dist/index.js

            # Step 6: Run tests to ensure quality
            - name: Verify tests
              run: npm test # Runs Jest tests

            # Step 7: Configure Git identity for releases
            - name: Configure Git User
              run: |
                  git config --global user.email "github-actions[bot]@users.noreply.github.com"
                  git config --global user.name "github-actions[bot]"

            # Step 8: Run semantic-release to analyze commits and publish to npm
            - name: Semantic Release
              id: semantic
              env:
                  # Tokens needed for GitHub and npm authentication
                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For creating releases and commenting
                  NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # For publishing to npm
              run: |
                  echo "Running semantic-release for version bump and npm publishing"
                  npx semantic-release

                  # Note: GitHub Packages publishing has been removed

```

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

```typescript
import { CreatePullRequestCommentToolArgs } from './atlassian.pullrequests.types';

describe('Atlassian Pull Requests Tool Types', () => {
	describe('CreatePullRequestCommentToolArgs Schema', () => {
		it('should accept valid parentId parameter for comment replies', () => {
			const validArgs = {
				repoSlug: 'test-repo',
				prId: '123',
				content: 'This is a reply to another comment',
				parentId: '456',
			};

			const result =
				CreatePullRequestCommentToolArgs.safeParse(validArgs);
			expect(result.success).toBe(true);
			if (result.success) {
				expect(result.data.parentId).toBe('456');
				expect(result.data.repoSlug).toBe('test-repo');
				expect(result.data.prId).toBe('123');
				expect(result.data.content).toBe(
					'This is a reply to another comment',
				);
			}
		});

		it('should work without parentId parameter for top-level comments', () => {
			const validArgs = {
				repoSlug: 'test-repo',
				prId: '123',
				content: 'This is a top-level comment',
			};

			const result =
				CreatePullRequestCommentToolArgs.safeParse(validArgs);
			expect(result.success).toBe(true);
			if (result.success) {
				expect(result.data.parentId).toBeUndefined();
				expect(result.data.repoSlug).toBe('test-repo');
				expect(result.data.prId).toBe('123');
				expect(result.data.content).toBe('This is a top-level comment');
			}
		});

		it('should accept both parentId and inline parameters together', () => {
			const validArgs = {
				repoSlug: 'test-repo',
				prId: '123',
				content: 'Reply with inline comment',
				parentId: '456',
				inline: {
					path: 'src/main.ts',
					line: 42,
				},
			};

			const result =
				CreatePullRequestCommentToolArgs.safeParse(validArgs);
			expect(result.success).toBe(true);
			if (result.success) {
				expect(result.data.parentId).toBe('456');
				expect(result.data.inline?.path).toBe('src/main.ts');
				expect(result.data.inline?.line).toBe(42);
			}
		});

		it('should require required fields even with parentId', () => {
			const invalidArgs = {
				parentId: '456', // parentId alone is not enough
			};

			const result =
				CreatePullRequestCommentToolArgs.safeParse(invalidArgs);
			expect(result.success).toBe(false);
		});

		it('should accept optional workspaceSlug with parentId', () => {
			const validArgs = {
				workspaceSlug: 'my-workspace',
				repoSlug: 'test-repo',
				prId: '123',
				content: 'Reply comment',
				parentId: '456',
			};

			const result =
				CreatePullRequestCommentToolArgs.safeParse(validArgs);
			expect(result.success).toBe(true);
			if (result.success) {
				expect(result.data.workspaceSlug).toBe('my-workspace');
				expect(result.data.parentId).toBe('456');
			}
		});
	});
});

```

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

```typescript
import { Logger } from './logger.util.js';
import { config } from './config.util.js';
import atlassianWorkspacesService from '../services/vendor.atlassian.workspaces.service.js';
import { WorkspaceMembership } from '../services/vendor.atlassian.workspaces.types.js';

const workspaceLogger = Logger.forContext('utils/workspace.util.ts');

/**
 * Cache for workspace data to avoid repeated API calls
 */
let cachedDefaultWorkspace: string | null = null;
let cachedWorkspaces: WorkspaceMembership[] | null = null;

/**
 * Get the default workspace slug
 *
 * This function follows this priority:
 * 1. Use cached value if available
 * 2. Check BITBUCKET_DEFAULT_WORKSPACE environment variable
 * 3. Fetch from API and use the first workspace in the list
 *
 * @returns {Promise<string|null>} The default workspace slug or null if not available
 */
export async function getDefaultWorkspace(): Promise<string | null> {
	const methodLogger = workspaceLogger.forMethod('getDefaultWorkspace');

	// Step 1: Return cached value if available
	if (cachedDefaultWorkspace) {
		methodLogger.debug(
			`Using cached default workspace: ${cachedDefaultWorkspace}`,
		);
		return cachedDefaultWorkspace;
	}

	// Step 2: Check environment variable
	const envWorkspace = config.get('BITBUCKET_DEFAULT_WORKSPACE');
	if (envWorkspace) {
		methodLogger.debug(
			`Using default workspace from environment: ${envWorkspace}`,
		);
		cachedDefaultWorkspace = envWorkspace;
		return envWorkspace;
	}

	// Step 3: Fetch from API
	methodLogger.debug('No default workspace configured, fetching from API...');
	try {
		const workspaces = await getWorkspaces();

		if (workspaces.length > 0) {
			const defaultWorkspace = workspaces[0].workspace.slug;
			methodLogger.debug(
				`Using first workspace from API as default: ${defaultWorkspace}`,
			);
			cachedDefaultWorkspace = defaultWorkspace;
			return defaultWorkspace;
		} else {
			methodLogger.warn('No workspaces found in the account');
			return null;
		}
	} catch (error) {
		methodLogger.error('Failed to fetch default workspace', error);
		return null;
	}
}

/**
 * Get list of workspaces from API or cache
 *
 * @returns {Promise<WorkspaceMembership[]>} Array of workspace membership objects
 */
export async function getWorkspaces(): Promise<WorkspaceMembership[]> {
	const methodLogger = workspaceLogger.forMethod('getWorkspaces');

	if (cachedWorkspaces) {
		methodLogger.debug(
			`Using ${cachedWorkspaces.length} cached workspaces`,
		);
		return cachedWorkspaces;
	}

	try {
		const result = await atlassianWorkspacesService.list({
			pagelen: 10, // Limit to first 10 workspaces
		});

		if (result.values) {
			cachedWorkspaces = result.values;
			methodLogger.debug(`Cached ${result.values.length} workspaces`);
			return result.values;
		} else {
			return [];
		}
	} catch (error) {
		methodLogger.error('Failed to fetch workspaces list', error);
		return [];
	}
}

```

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

```typescript
import { ControllerResponse } from '../types/common.types.js';
import { GetPullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
import { GetPullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
import {
	atlassianPullRequestsService,
	Logger,
	handleControllerError,
	formatPullRequestDetails,
	applyDefaults,
	getDefaultWorkspace,
} from './atlassian.pullrequests.base.controller.js';

/**
 * Get detailed information about a specific Bitbucket pull request
 * @param options - Options including workspace slug, repo slug, and pull request ID
 * @returns Promise with formatted pull request details as Markdown content
 */
async function get(
	options: GetPullRequestToolArgsType,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.pullrequests.get.controller.ts',
		'get',
	);

	try {
		// Apply default values if needed
		const mergedOptions = applyDefaults<GetPullRequestToolArgsType>(
			options,
			{}, // No defaults required for this operation
		);

		// Handle optional workspaceSlug - get default if not provided
		if (!mergedOptions.workspaceSlug) {
			methodLogger.debug(
				'No workspace provided, fetching default workspace',
			);
			const defaultWorkspace = await getDefaultWorkspace();
			if (!defaultWorkspace) {
				throw new Error(
					'Could not determine a default workspace. Please provide a workspaceSlug.',
				);
			}
			mergedOptions.workspaceSlug = defaultWorkspace;
			methodLogger.debug(
				`Using default workspace: ${mergedOptions.workspaceSlug}`,
			);
		}

		const { workspaceSlug, repoSlug, prId } = mergedOptions;

		// Validate required parameters
		if (!workspaceSlug || !repoSlug || !prId) {
			throw new Error(
				'Workspace slug, repository slug, and pull request ID are required',
			);
		}

		methodLogger.debug(
			`Getting pull request details for ${workspaceSlug}/${repoSlug}/${prId}`,
		);

		// Map controller options to service parameters
		const serviceParams: GetPullRequestParams = {
			workspace: workspaceSlug,
			repo_slug: repoSlug,
			pull_request_id: parseInt(prId, 10),
		};

		// Get PR details from the service
		const pullRequestData =
			await atlassianPullRequestsService.get(serviceParams);

		methodLogger.debug('Retrieved pull request details', {
			id: pullRequestData.id,
			title: pullRequestData.title,
			state: pullRequestData.state,
		});

		// Format the pull request details using the formatter
		const formattedContent = formatPullRequestDetails(pullRequestData);

		return {
			content: formattedContent,
		};
	} catch (error) {
		// Use the standardized error handler
		throw handleControllerError(error, {
			entityType: 'Pull Request',
			operation: 'retrieving details',
			source: 'controllers/atlassian.pullrequests.get.controller.ts@get',
			additionalInfo: { options },
		});
	}
}

// Export the controller functions
export default { get };

```

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

```typescript
import { Logger } from '../utils/logger.util.js';
import { ControllerResponse } from '../types/common.types.js';
import { DEFAULT_PAGE_SIZE } from '../utils/defaults.util.js';
import {
	extractPaginationInfo,
	PaginationType,
} from '../utils/pagination.util.js';
import { formatPagination } from '../utils/formatter.util.js';
import { formatRepositoriesList } from './atlassian.repositories.formatter.js';
import { RepositoriesResponse } from '../services/vendor.atlassian.repositories.types.js';
import {
	fetchAtlassian,
	getAtlassianCredentials,
} from '../utils/transport.util.js';

/**
 * Handle search for repositories (limited functionality in the API)
 */
export async function handleRepositorySearch(
	workspaceSlug: string,
	_repoSlug?: string, // Renamed to indicate it's intentionally unused
	query?: string,
	limit: number = DEFAULT_PAGE_SIZE,
	cursor?: string,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.search.repositories.controller.ts',
		'handleRepositorySearch',
	);
	methodLogger.debug('Performing repository search');

	if (!query) {
		return {
			content: 'Please provide a search query for repository search.',
		};
	}

	try {
		const credentials = getAtlassianCredentials();
		if (!credentials) {
			throw new Error(
				'Atlassian credentials are required for this operation',
			);
		}

		// Build query params
		const queryParams = new URLSearchParams();

		// Format the query - Bitbucket's repository API allows filtering by name/description
		const formattedQuery = `(name ~ "${query}" OR description ~ "${query}")`;
		queryParams.set('q', formattedQuery);

		// Add pagination parameters
		queryParams.set('pagelen', limit.toString());
		if (cursor) {
			queryParams.set('page', cursor);
		}

		// Sort by most recently updated
		queryParams.set('sort', '-updated_on');

		// Use the repositories endpoint to search
		const path = `/2.0/repositories/${workspaceSlug}?${queryParams.toString()}`;

		methodLogger.debug(`Sending repository search request: ${path}`);

		const searchData = await fetchAtlassian<RepositoriesResponse>(
			credentials,
			path,
		);

		methodLogger.debug(
			`Search complete, found ${searchData.values?.length || 0} matches`,
		);

		// Extract pagination information
		const pagination = extractPaginationInfo(
			searchData,
			PaginationType.PAGE,
		);

		// Format the search results
		const formattedRepos = formatRepositoriesList(searchData);
		let finalContent = `# Repository Search Results\n\n${formattedRepos}`;

		// Add pagination information if available
		if (
			pagination &&
			(pagination.hasMore || pagination.count !== undefined)
		) {
			const paginationString = formatPagination(pagination);
			finalContent += '\n\n' + paginationString;
		}

		return {
			content: finalContent,
		};
	} catch (searchError) {
		methodLogger.error('Error performing repository search:', searchError);
		throw searchError;
	}
}

```

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

```typescript
import { ControllerResponse } from '../types/common.types.js';
import { ApprovePullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
import { ApprovePullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
import {
	atlassianPullRequestsService,
	Logger,
	handleControllerError,
	applyDefaults,
	getDefaultWorkspace,
} from './atlassian.pullrequests.base.controller.js';

/**
 * Approve a pull request in Bitbucket
 * @param options - Options including workspace slug, repo slug, and pull request ID
 * @returns Promise with formatted approval confirmation as Markdown content
 */
async function approve(
	options: ApprovePullRequestToolArgsType,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.pullrequests.approve.controller.ts',
		'approve',
	);

	try {
		// Apply defaults if needed (none for this operation)
		const mergedOptions = applyDefaults<ApprovePullRequestToolArgsType>(
			options,
			{},
		);

		// Handle optional workspaceSlug - get default if not provided
		if (!mergedOptions.workspaceSlug) {
			methodLogger.debug(
				'No workspace provided, fetching default workspace',
			);
			const defaultWorkspace = await getDefaultWorkspace();
			if (!defaultWorkspace) {
				throw new Error(
					'Could not determine a default workspace. Please provide a workspaceSlug.',
				);
			}
			mergedOptions.workspaceSlug = defaultWorkspace;
			methodLogger.debug(
				`Using default workspace: ${mergedOptions.workspaceSlug}`,
			);
		}

		methodLogger.debug(
			`Approving pull request ${mergedOptions.pullRequestId} in ${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}`,
		);

		// Prepare service parameters
		const serviceParams: ApprovePullRequestParams = {
			workspace: mergedOptions.workspaceSlug,
			repo_slug: mergedOptions.repoSlug,
			pull_request_id: mergedOptions.pullRequestId,
		};

		// Call service to approve the pull request
		const participant =
			await atlassianPullRequestsService.approve(serviceParams);

		methodLogger.debug(
			`Successfully approved pull request ${mergedOptions.pullRequestId}`,
		);

		// Format the response
		const content = `# Pull Request Approved ✅

**Pull Request ID:** ${mergedOptions.pullRequestId}
**Repository:** \`${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}\`
**Approved by:** ${participant.user.display_name || participant.user.nickname || 'Unknown User'}
**Status:** ${participant.state}
**Participated on:** ${new Date(participant.participated_on).toLocaleString()}

The pull request has been successfully approved and is now ready for merge (pending any other required approvals or checks).`;

		return {
			content: content,
		};
	} catch (error) {
		throw handleControllerError(error, {
			entityType: 'Pull Request',
			operation: 'approving',
			source: 'controllers/atlassian.pullrequests.approve.controller.ts@approve',
			additionalInfo: { options },
		});
	}
}

export default { approve };

```

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

```typescript
import { ControllerResponse } from '../types/common.types.js';
import { RejectPullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
import { RejectPullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
import {
	atlassianPullRequestsService,
	Logger,
	handleControllerError,
	applyDefaults,
	getDefaultWorkspace,
} from './atlassian.pullrequests.base.controller.js';

/**
 * Request changes on a pull request in Bitbucket
 * @param options - Options including workspace slug, repo slug, and pull request ID
 * @returns Promise with formatted rejection confirmation as Markdown content
 */
async function reject(
	options: RejectPullRequestToolArgsType,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.pullrequests.reject.controller.ts',
		'reject',
	);

	try {
		// Apply defaults if needed (none for this operation)
		const mergedOptions = applyDefaults<RejectPullRequestToolArgsType>(
			options,
			{},
		);

		// Handle optional workspaceSlug - get default if not provided
		if (!mergedOptions.workspaceSlug) {
			methodLogger.debug(
				'No workspace provided, fetching default workspace',
			);
			const defaultWorkspace = await getDefaultWorkspace();
			if (!defaultWorkspace) {
				throw new Error(
					'Could not determine a default workspace. Please provide a workspaceSlug.',
				);
			}
			mergedOptions.workspaceSlug = defaultWorkspace;
			methodLogger.debug(
				`Using default workspace: ${mergedOptions.workspaceSlug}`,
			);
		}

		methodLogger.debug(
			`Requesting changes on pull request ${mergedOptions.pullRequestId} in ${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}`,
		);

		// Prepare service parameters
		const serviceParams: RejectPullRequestParams = {
			workspace: mergedOptions.workspaceSlug,
			repo_slug: mergedOptions.repoSlug,
			pull_request_id: mergedOptions.pullRequestId,
		};

		// Call service to request changes on the pull request
		const participant =
			await atlassianPullRequestsService.reject(serviceParams);

		methodLogger.debug(
			`Successfully requested changes on pull request ${mergedOptions.pullRequestId}`,
		);

		// Format the response
		const content = `# Changes Requested 🔄

**Pull Request ID:** ${mergedOptions.pullRequestId}
**Repository:** \`${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}\`
**Requested by:** ${participant.user.display_name || participant.user.nickname || 'Unknown User'}
**Status:** ${participant.state}
**Participated on:** ${new Date(participant.participated_on).toLocaleString()}

Changes have been requested on this pull request. The author should address the feedback before the pull request can be merged.`;

		return {
			content: content,
		};
	} catch (error) {
		throw handleControllerError(error, {
			entityType: 'Pull Request',
			operation: 'requesting changes on',
			source: 'controllers/atlassian.pullrequests.reject.controller.ts@reject',
			additionalInfo: { options },
		});
	}
}

export default { reject };

```

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

```typescript
import * as path from 'path';
import { pathToString, isPathInHome, formatDisplayPath } from './path.util.js';

describe('Path Utilities', () => {
	describe('pathToString', () => {
		it('should convert string paths correctly', () => {
			expect(pathToString('/test/path')).toBe('/test/path');
		});

		it('should join array paths correctly', () => {
			expect(pathToString(['/test', 'path'])).toBe(
				path.join('/test', 'path'),
			);
		});

		it('should handle URL objects', () => {
			expect(pathToString(new URL('file:///test/path'))).toBe(
				'/test/path',
			);
		});

		it('should handle objects with toString', () => {
			expect(pathToString({ toString: () => '/test/path' })).toBe(
				'/test/path',
			);
		});

		it('should convert null or undefined to empty string', () => {
			expect(pathToString(null)).toBe('');
			expect(pathToString(undefined)).toBe('');
		});
	});

	describe('isPathInHome', () => {
		const originalHome = process.env.HOME;
		const originalUserProfile = process.env.USERPROFILE;

		beforeEach(() => {
			// Set a mock home directory for testing
			process.env.HOME = '/mock/home';
			process.env.USERPROFILE = '/mock/home';
		});

		afterEach(() => {
			// Restore the original environment variables
			process.env.HOME = originalHome;
			process.env.USERPROFILE = originalUserProfile;
		});

		it('should return true for paths in home directory', () => {
			expect(isPathInHome('/mock/home/projects')).toBe(true);
		});

		it('should return false for paths outside home directory', () => {
			expect(isPathInHome('/tmp/projects')).toBe(false);
		});

		it('should resolve relative paths correctly', () => {
			const cwd = process.cwd();
			if (cwd.startsWith('/mock/home')) {
				expect(isPathInHome('./projects')).toBe(true);
			} else {
				expect(isPathInHome('./projects')).toBe(false);
			}
		});
	});

	describe('formatDisplayPath', () => {
		const originalHome = process.env.HOME;
		const originalUserProfile = process.env.USERPROFILE;

		beforeEach(() => {
			// Set a mock home directory for testing
			process.env.HOME = '/mock/home';
			process.env.USERPROFILE = '/mock/home';
		});

		afterEach(() => {
			// Restore the original environment variables
			process.env.HOME = originalHome;
			process.env.USERPROFILE = originalUserProfile;
		});

		it('should replace home directory with tilde when requested', () => {
			expect(formatDisplayPath('/mock/home/projects', true)).toBe(
				'~/projects',
			);
		});

		it('should not replace home directory when not requested', () => {
			expect(formatDisplayPath('/mock/home/projects', false)).toBe(
				'/mock/home/projects',
			);
		});

		it('should not modify paths outside of home directory', () => {
			expect(formatDisplayPath('/tmp/projects', true)).toBe(
				'/tmp/projects',
			);
		});

		it('should resolve relative paths', () => {
			// This will resolve to the absolute path based on current working directory
			const expected = path.resolve('./projects');
			expect(formatDisplayPath('./projects')).toBe(expected);
		});
	});
});

```

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

```typescript
import atlassianRepositoriesService from '../services/vendor.atlassian.repositories.service.js';
import atlassianPullRequestsService from '../services/vendor.atlassian.pullrequests.service.js';
import { Logger } from '../utils/logger.util.js';
import { handleControllerError } from '../utils/error-handler.util.js';
import { ControllerResponse } from '../types/common.types.js';
import { GetRepositoryToolArgsType } from '../tools/atlassian.repositories.types.js';
import { formatRepositoryDetails } from './atlassian.repositories.formatter.js';
import { getDefaultWorkspace } from '../utils/workspace.util.js';

// Logger instance for this module
const logger = Logger.forContext(
	'controllers/atlassian.repositories.details.controller.ts',
);

/**
 * Get details of a specific repository
 *
 * @param params - Parameters containing workspaceSlug and repoSlug
 * @returns Promise with formatted repository details content
 */
export async function handleRepositoryDetails(
	params: GetRepositoryToolArgsType,
): Promise<ControllerResponse> {
	const methodLogger = logger.forMethod('handleRepositoryDetails');

	try {
		methodLogger.debug('Getting repository details', params);

		// Handle optional workspaceSlug
		if (!params.workspaceSlug) {
			methodLogger.debug(
				'No workspace provided, fetching default workspace',
			);
			const defaultWorkspace = await getDefaultWorkspace();
			if (!defaultWorkspace) {
				throw new Error(
					'No default workspace found. Please provide a workspace slug.',
				);
			}
			params.workspaceSlug = defaultWorkspace;
			methodLogger.debug(`Using default workspace: ${defaultWorkspace}`);
		}

		// Call the service to get repository details
		const repoData = await atlassianRepositoriesService.get({
			workspace: params.workspaceSlug,
			repo_slug: params.repoSlug,
		});

		// Fetch recent pull requests for this repository (most recently updated, limit to 5)
		let pullRequestsData = null;
		try {
			methodLogger.debug(
				'Fetching recent pull requests for the repository',
			);
			pullRequestsData = await atlassianPullRequestsService.list({
				workspace: params.workspaceSlug,
				repo_slug: params.repoSlug,
				state: 'OPEN', // Focus on open PRs
				sort: '-updated_on', // Sort by most recently updated
				pagelen: 5, // Limit to 5 to keep the response concise
			});
			methodLogger.debug(
				`Retrieved ${pullRequestsData.values?.length || 0} recent pull requests`,
			);
		} catch (error) {
			// Log the error but continue - this is an enhancement, not critical
			methodLogger.warn(
				'Failed to fetch recent pull requests, continuing without them',
				error,
			);
			// Do not fail the entire operation if pull requests cannot be fetched
		}

		// Format the repository data with optional pull requests
		const content = formatRepositoryDetails(repoData, pullRequestsData);

		return { content };
	} catch (error) {
		throw handleControllerError(error, {
			entityType: 'Repository',
			operation: 'get',
			source: 'controllers/atlassian.repositories.details.controller.ts@handleRepositoryDetails',
			additionalInfo: params,
		});
	}
}

```

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

```typescript
import { CliTestUtil } from '../utils/cli.test.util';
import { getAtlassianCredentials } from '../utils/transport.util';

describe('Atlassian Search CLI Commands', () => {
	beforeAll(() => {
		// Check if credentials are available
		const credentials = getAtlassianCredentials();
		if (!credentials) {
			console.warn(
				'WARNING: No Atlassian credentials available. Live API tests will be skipped.',
			);
		}
	});

	/**
	 * Helper function to skip tests if Atlassian credentials are not available
	 */
	const skipIfNoCredentials = () => {
		const credentials = getAtlassianCredentials();
		if (!credentials) {
			return true;
		}
		return false;
	};

	describe('search command', () => {
		it('should search repositories and return success exit code', async () => {
			if (skipIfNoCredentials()) {
				return; // Skip silently - no credentials available
			}

			const { stdout, exitCode } = await CliTestUtil.runCommand([
				'search',
				'--query',
				'test',
			]);

			expect(exitCode).toBe(0);
			CliTestUtil.validateMarkdownOutput(stdout);
			CliTestUtil.validateOutputContains(stdout, ['## Search Results']);
		}, 60000);

		it('should support searching with query parameter', async () => {
			if (skipIfNoCredentials()) {
				return; // Skip silently - no credentials available
			}

			const { stdout, exitCode } = await CliTestUtil.runCommand([
				'search',
				'--query',
				'api',
			]);

			expect(exitCode).toBe(0);
			CliTestUtil.validateMarkdownOutput(stdout);
			CliTestUtil.validateOutputContains(stdout, ['## Search Results']);
		}, 60000);

		it('should support pagination with limit flag', async () => {
			if (skipIfNoCredentials()) {
				return; // Skip silently - no credentials available
			}

			const { stdout, exitCode } = await CliTestUtil.runCommand([
				'search',
				'--query',
				'test',
				'--limit',
				'2',
			]);

			expect(exitCode).toBe(0);
			CliTestUtil.validateMarkdownOutput(stdout);
			// Check for pagination markers
			CliTestUtil.validateOutputContains(stdout, [
				/Showing \d+ results/,
				/Next page:|No more results/,
			]);
		}, 60000);

		it('should require the query parameter', async () => {
			const { stderr, exitCode } = await CliTestUtil.runCommand([
				'search',
			]);

			expect(exitCode).not.toBe(0);
			expect(stderr).toMatch(
				/required option|missing required|specify a query/i,
			);
		}, 30000);

		it('should handle invalid limit value gracefully', async () => {
			if (skipIfNoCredentials()) {
				return; // Skip silently - no credentials available
			}

			const { stdout, exitCode } = await CliTestUtil.runCommand([
				'search',
				'--query',
				'test',
				'--limit',
				'not-a-number',
			]);

			expect(exitCode).not.toBe(0);
			CliTestUtil.validateOutputContains(stdout, [
				/Error|Invalid|Failed/i,
			]);
		}, 60000);

		it('should handle help flag correctly', async () => {
			const { stdout, exitCode } = await CliTestUtil.runCommand([
				'search',
				'--help',
			]);

			expect(exitCode).toBe(0);
			expect(stdout).toMatch(/Usage|Options|Description/i);
			expect(stdout).toContain('search');
		}, 15000);
	});
});

```

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

```typescript
import { ControllerResponse } from '../types/common.types.js';
import { UpdatePullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
import { UpdatePullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
import {
	atlassianPullRequestsService,
	Logger,
	handleControllerError,
	formatPullRequestDetails,
	applyDefaults,
	optimizeBitbucketMarkdown,
	getDefaultWorkspace,
} from './atlassian.pullrequests.base.controller.js';

/**
 * Update an existing pull request in Bitbucket
 * @param options - Options including workspace slug, repo slug, pull request ID, title, and description
 * @returns Promise with formatted updated pull request details as Markdown content
 */
async function update(
	options: UpdatePullRequestToolArgsType,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.pullrequests.update.controller.ts',
		'update',
	);

	try {
		// Apply defaults if needed (none for this operation)
		const mergedOptions = applyDefaults<UpdatePullRequestToolArgsType>(
			options,
			{},
		);

		// Handle optional workspaceSlug - get default if not provided
		if (!mergedOptions.workspaceSlug) {
			methodLogger.debug(
				'No workspace provided, fetching default workspace',
			);
			const defaultWorkspace = await getDefaultWorkspace();
			if (!defaultWorkspace) {
				throw new Error(
					'Could not determine a default workspace. Please provide a workspaceSlug.',
				);
			}
			mergedOptions.workspaceSlug = defaultWorkspace;
			methodLogger.debug(
				`Using default workspace: ${mergedOptions.workspaceSlug}`,
			);
		}

		// Validate that at least one field to update is provided
		if (!mergedOptions.title && !mergedOptions.description) {
			throw new Error(
				'At least one field to update (title or description) must be provided',
			);
		}

		methodLogger.debug(
			`Updating pull request ${mergedOptions.pullRequestId} in ${mergedOptions.workspaceSlug}/${mergedOptions.repoSlug}`,
		);

		// Prepare service parameters
		const serviceParams: UpdatePullRequestParams = {
			workspace: mergedOptions.workspaceSlug,
			repo_slug: mergedOptions.repoSlug,
			pull_request_id: mergedOptions.pullRequestId,
		};

		// Add optional fields if provided
		if (mergedOptions.title !== undefined) {
			serviceParams.title = mergedOptions.title;
		}
		if (mergedOptions.description !== undefined) {
			serviceParams.description = optimizeBitbucketMarkdown(
				mergedOptions.description,
			);
		}

		// Call service to update the pull request
		const pullRequest =
			await atlassianPullRequestsService.update(serviceParams);

		methodLogger.debug(
			`Successfully updated pull request ${pullRequest.id}`,
		);

		// Format the response
		const content = await formatPullRequestDetails(pullRequest);

		return {
			content: `## Pull Request Updated Successfully\n\n${content}`,
		};
	} catch (error) {
		throw handleControllerError(error, {
			entityType: 'Pull Request',
			operation: 'updating',
			source: 'controllers/atlassian.pullrequests.update.controller.ts@update',
			additionalInfo: { options },
		});
	}
}

export default { update };

```

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

```typescript
import { z } from 'zod';

/**
 * Types for Atlassian Bitbucket Workspaces API
 */

/**
 * Workspace type (basic object)
 */
export const WorkspaceTypeSchema = z.literal('workspace');
export type WorkspaceType = z.infer<typeof WorkspaceTypeSchema>;

/**
 * Workspace user object
 */
export const WorkspaceUserSchema = z.object({
	type: z.literal('user'),
	uuid: z.string(),
	nickname: z.string(),
	display_name: z.string(),
});

/**
 * Workspace permission type
 */
export const WorkspacePermissionSchema = z.enum([
	'owner',
	'collaborator',
	'member',
]);

/**
 * Workspace links object
 */
const LinkSchema = z.object({
	href: z.string(),
	name: z.string().optional(),
});

export const WorkspaceLinksSchema = z.object({
	avatar: LinkSchema.optional(),
	html: LinkSchema.optional(),
	members: LinkSchema.optional(),
	owners: LinkSchema.optional(),
	projects: LinkSchema.optional(),
	repositories: LinkSchema.optional(),
	snippets: LinkSchema.optional(),
	self: LinkSchema.optional(),
});
export type WorkspaceLinks = z.infer<typeof WorkspaceLinksSchema>;

/**
 * Workspace forking mode
 */
export const WorkspaceForkingModeSchema = z.enum([
	'allow_forks',
	'no_public_forks',
	'no_forks',
]);
export type WorkspaceForkingMode = z.infer<typeof WorkspaceForkingModeSchema>;

/**
 * Workspace object returned from the API
 */
export const WorkspaceSchema: z.ZodType<{
	type: WorkspaceType;
	uuid: string;
	name: string;
	slug: string;
	is_private?: boolean;
	is_privacy_enforced?: boolean;
	forking_mode?: WorkspaceForkingMode;
	created_on?: string;
	updated_on?: string;
	links: WorkspaceLinks;
}> = z.object({
	type: WorkspaceTypeSchema,
	uuid: z.string(),
	name: z.string(),
	slug: z.string(),
	is_private: z.boolean().optional(),
	is_privacy_enforced: z.boolean().optional(),
	forking_mode: WorkspaceForkingModeSchema.optional(),
	created_on: z.string().optional(),
	updated_on: z.string().optional(),
	links: WorkspaceLinksSchema,
});

/**
 * Workspace membership object
 */
export const WorkspaceMembershipSchema = z.object({
	type: z.literal('workspace_membership'),
	permission: WorkspacePermissionSchema,
	last_accessed: z.string().optional(),
	added_on: z.string().optional(),
	user: WorkspaceUserSchema,
	workspace: WorkspaceSchema,
});
export type WorkspaceMembership = z.infer<typeof WorkspaceMembershipSchema>;

/**
 * Extended workspace object with optional fields
 * @remarks Currently identical to Workspace, but allows for future extension
 */
export const WorkspaceDetailedSchema = WorkspaceSchema;
export type WorkspaceDetailed = z.infer<typeof WorkspaceDetailedSchema>;

/**
 * Parameters for listing workspaces
 */
export const ListWorkspacesParamsSchema = z.object({
	q: z.string().optional(),
	page: z.number().optional(),
	pagelen: z.number().optional(),
});
export type ListWorkspacesParams = z.infer<typeof ListWorkspacesParamsSchema>;

/**
 * API response for user permissions on workspaces
 */
export const WorkspacePermissionsResponseSchema = z.object({
	pagelen: z.number(),
	page: z.number(),
	size: z.number(),
	next: z.string().optional(),
	previous: z.string().optional(),
	values: z.array(WorkspaceMembershipSchema),
});
export type WorkspacePermissionsResponse = z.infer<
	typeof WorkspacePermissionsResponseSchema
>;

```

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

```typescript
import { Logger } from './logger.util.js';

const utilLogger = Logger.forContext('utils/diff.util.ts');

/**
 * Extracts a code snippet from raw unified diff content around a specific line number.
 *
 * @param diffContent - The raw unified diff content (string).
 * @param targetLineNumber - The line number (in the "new" file) to center the snippet around.
 * @param contextLines - The number of lines to include before and after the target line.
 * @returns The extracted code snippet as a string, or an empty string if extraction fails.
 */
export function extractDiffSnippet(
	diffContent: string,
	targetLineNumber: number,
	contextLines = 2,
): string {
	const methodLogger = utilLogger.forMethod('extractDiffSnippet');
	methodLogger.debug(
		`Attempting to extract snippet around line ${targetLineNumber}`,
	);
	const lines = diffContent.split('\n');
	const snippetLines: string[] = [];
	let currentNewLineNumber = 0;
	let hunkHeaderFound = false;

	for (const line of lines) {
		if (line.startsWith('@@')) {
			// Found a hunk header, parse the starting line number of the new file
			const match = line.match(/\+([0-9]+)/); // Matches the part like "+1,10" or "+5"
			if (match && match[1]) {
				currentNewLineNumber = parseInt(match[1], 10) - 1; // -1 because we increment before checking
				hunkHeaderFound = true;
				methodLogger.debug(
					`Found hunk starting at new line number: ${currentNewLineNumber + 1}`,
				);
			} else {
				methodLogger.warn('Could not parse hunk header:', line);
				hunkHeaderFound = false; // Reset if header is unparseable
			}
			continue; // Skip the hunk header line itself
		}

		if (!hunkHeaderFound) {
			continue; // Skip lines before the first valid hunk header
		}

		// Track line numbers only for lines added or unchanged in the new file
		if (line.startsWith('+') || line.startsWith(' ')) {
			currentNewLineNumber++;
			// Check if the current line is within the desired context range
			if (
				currentNewLineNumber >= targetLineNumber - contextLines &&
				currentNewLineNumber <= targetLineNumber + contextLines
			) {
				// Prepend line numbers for context, marking the target line
				const prefix =
					currentNewLineNumber === targetLineNumber ? '>' : ' ';
				// Add the line, removing the diff marker (+ or space)
				snippetLines.push(
					`${prefix} ${currentNewLineNumber.toString().padStart(4)}: ${line.substring(1)}`,
				);
			}
		} else if (line.startsWith('-')) {
			// Lines only in the old file don't increment the new file line number
			// but can be included for context if they fall within the range calculation *based on previous new lines*
			// This is complex logic, for now, we only show '+' and ' ' lines for simplicity.
			// Future enhancement: Show '-' lines that are adjacent to the target context.
		}

		// Optimization: if we've passed the target context range, stop processing
		if (currentNewLineNumber > targetLineNumber + contextLines) {
			methodLogger.debug(
				`Passed target context range (current: ${currentNewLineNumber}, target: ${targetLineNumber}). Stopping search.`,
			);
			break;
		}
	}

	if (snippetLines.length === 0) {
		methodLogger.warn(
			`Could not find or extract snippet for line ${targetLineNumber}`,
		);
		return ''; // Return empty if no relevant lines found
	}

	methodLogger.debug(
		`Successfully extracted snippet with ${snippetLines.length} lines.`,
	);
	return snippetLines.join('\n');
}

```

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

```typescript
import { Command } from 'commander';
import { Logger } from '../utils/logger.util.js';
import { handleCliError } from '../utils/error.util.js';
import atlassianWorkspacesController from '../controllers/atlassian.workspaces.controller.js';

/**
 * CLI module for managing Bitbucket workspaces.
 * Provides commands for listing workspaces and retrieving workspace details.
 * All commands require valid Atlassian credentials.
 */

// Create a contextualized logger for this file
const cliLogger = Logger.forContext('cli/atlassian.workspaces.cli.ts');

// Log CLI initialization
cliLogger.debug('Bitbucket workspaces CLI module initialized');

/**
 * Register Bitbucket workspaces CLI commands with the Commander program
 *
 * @param program - The Commander program instance to register commands with
 * @throws Error if command registration fails
 */
function register(program: Command): void {
	const methodLogger = Logger.forContext(
		'cli/atlassian.workspaces.cli.ts',
		'register',
	);
	methodLogger.debug('Registering Bitbucket Workspaces CLI commands...');

	registerListWorkspacesCommand(program);
	registerGetWorkspaceCommand(program);

	methodLogger.debug('CLI commands registered successfully');
}

/**
 * Register the command for listing Bitbucket workspaces
 *
 * @param program - The Commander program instance
 */
function registerListWorkspacesCommand(program: Command): void {
	program
		.command('ls-workspaces')
		.description('List workspaces in your Bitbucket account.')
		.option(
			'-l, --limit <number>',
			'Maximum number of workspaces to retrieve (1-100). Default: 25.',
		)
		.option(
			'-c, --cursor <string>',
			'Pagination cursor for retrieving the next set of results.',
		)
		.action(async (options) => {
			const actionLogger = cliLogger.forMethod('ls-workspaces');
			try {
				actionLogger.debug('Processing command options:', options);

				// Map CLI options to controller params - keep only type conversions
				const controllerOptions = {
					limit: options.limit
						? parseInt(options.limit, 10)
						: undefined,
					cursor: options.cursor,
				};

				// Call controller directly
				const result =
					await atlassianWorkspacesController.list(controllerOptions);

				console.log(result.content);
			} catch (error) {
				actionLogger.error('Operation failed:', error);
				handleCliError(error);
			}
		});
}

/**
 * Register the command for retrieving a specific Bitbucket workspace
 *
 * @param program - The Commander program instance
 */
function registerGetWorkspaceCommand(program: Command): void {
	program
		.command('get-workspace')
		.description(
			'Get detailed information about a specific Bitbucket workspace.',
		)
		.requiredOption(
			'-w, --workspace-slug <slug>',
			'Workspace slug to retrieve. Must be a valid workspace slug from your Bitbucket account. Example: "myteam"',
		)
		.action(async (options) => {
			const actionLogger = Logger.forContext(
				'cli/atlassian.workspaces.cli.ts',
				'get-workspace',
			);
			try {
				actionLogger.debug(
					`Fetching workspace: ${options.workspaceSlug}`,
				);

				// Call controller directly with passed options
				const result = await atlassianWorkspacesController.get({
					workspaceSlug: options.workspaceSlug,
				});

				console.log(result.content);
			} catch (error) {
				actionLogger.error('Operation failed:', error);
				handleCliError(error);
			}
		});
}

export default { register };

```

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

```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logger } from '../utils/logger.util.js';
import { formatErrorForMcpTool } from '../utils/error.util.js';
import diffController from '../controllers/atlassian.diff.controller.js';
import {
	BranchDiffArgsSchema,
	CommitDiffArgsSchema,
	type BranchDiffArgsType,
	type CommitDiffArgsType,
} from './atlassian.diff.types.js';

// Create a contextualized logger for this file
const toolLogger = Logger.forContext('tools/atlassian.diff.tool.ts');

// Log tool initialization
toolLogger.debug('Bitbucket diff tool initialized');

/**
 * Handles branch diff requests
 * @param args - Arguments for the branch diff operation
 * @returns MCP tool response
 */
async function branchDiff(args: Record<string, unknown>) {
	const methodLogger = toolLogger.forMethod('branchDiff');
	try {
		methodLogger.debug('Processing branch diff tool request', args);

		// Pass args directly to controller without any business logic
		const result = await diffController.branchDiff(
			args as BranchDiffArgsType,
		);

		methodLogger.debug(
			'Successfully retrieved branch diff from controller',
		);

		return {
			content: [
				{
					type: 'text' as const,
					text: result.content,
				},
			],
		};
	} catch (error) {
		methodLogger.error('Failed to retrieve branch diff', error);
		return formatErrorForMcpTool(error);
	}
}

/**
 * Handles commit diff requests
 * @param args - Arguments for the commit diff operation
 * @returns MCP tool response
 */
async function commitDiff(args: Record<string, unknown>) {
	const methodLogger = toolLogger.forMethod('commitDiff');
	try {
		methodLogger.debug('Processing commit diff tool request', args);

		// Pass args directly to controller without any business logic
		const result = await diffController.commitDiff(
			args as CommitDiffArgsType,
		);

		methodLogger.debug(
			'Successfully retrieved commit diff from controller',
		);

		return {
			content: [
				{
					type: 'text' as const,
					text: result.content,
				},
			],
		};
	} catch (error) {
		methodLogger.error('Failed to retrieve commit diff', error);
		return formatErrorForMcpTool(error);
	}
}

/**
 * Register all Bitbucket diff tools with the MCP server.
 */
function registerTools(server: McpServer) {
	const registerLogger = Logger.forContext(
		'tools/atlassian.diff.tool.ts',
		'registerTools',
	);
	registerLogger.debug('Registering Diff tools...');

	// Register the branch diff tool
	server.tool(
		'bb_diff_branches',
		`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.`,
		BranchDiffArgsSchema.shape,
		branchDiff,
	);

	// Register the commit diff tool
	server.tool(
		'bb_diff_commits',
		`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.`,
		CommitDiffArgsSchema.shape,
		commitDiff,
	);

	registerLogger.debug('Successfully registered Diff tools');
}

export default { registerTools };

```

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

```typescript
import atlassianRepositoriesService from '../services/vendor.atlassian.repositories.service.js';
import { Logger } from '../utils/logger.util.js';
import { handleControllerError } from '../utils/error-handler.util.js';
import { DEFAULT_PAGE_SIZE, applyDefaults } from '../utils/defaults.util.js';
import {
	extractPaginationInfo,
	PaginationType,
} from '../utils/pagination.util.js';
import { formatPagination } from '../utils/formatter.util.js';
import { ControllerResponse } from '../types/common.types.js';
import { GetCommitHistoryToolArgsType } from '../tools/atlassian.repositories.types.js';
import { formatCommitHistory } from './atlassian.repositories.formatter.js';
import { ListCommitsParams } from '../services/vendor.atlassian.repositories.types.js';
import { getDefaultWorkspace } from '../utils/workspace.util.js';

// Logger instance for this module
const logger = Logger.forContext(
	'controllers/atlassian.repositories.commit.controller.ts',
);

/**
 * Get commit history for a repository
 *
 * @param options - Options containing repository identifiers and filters
 * @returns Promise with formatted commit history content and pagination info
 */
export async function handleCommitHistory(
	options: GetCommitHistoryToolArgsType,
): Promise<ControllerResponse> {
	const methodLogger = logger.forMethod('handleCommitHistory');

	try {
		methodLogger.debug('Getting commit history', options);

		// Apply defaults
		const defaults = {
			limit: DEFAULT_PAGE_SIZE,
		};
		const params = applyDefaults(
			options,
			defaults,
		) as GetCommitHistoryToolArgsType & {
			limit: number;
		};

		// Handle optional workspaceSlug
		if (!params.workspaceSlug) {
			methodLogger.debug(
				'No workspace provided, fetching default workspace',
			);
			const defaultWorkspace = await getDefaultWorkspace();
			if (!defaultWorkspace) {
				throw new Error(
					'No default workspace found. Please provide a workspace slug.',
				);
			}
			params.workspaceSlug = defaultWorkspace;
			methodLogger.debug(`Using default workspace: ${defaultWorkspace}`);
		}

		const serviceParams: ListCommitsParams = {
			workspace: params.workspaceSlug,
			repo_slug: params.repoSlug,
			include: params.revision,
			path: params.path,
			pagelen: params.limit,
			page: params.cursor ? parseInt(params.cursor, 10) : undefined,
		};

		methodLogger.debug('Fetching commits with params:', serviceParams);
		const commitsData =
			await atlassianRepositoriesService.listCommits(serviceParams);
		methodLogger.debug(
			`Retrieved ${commitsData.values?.length || 0} commits`,
		);

		// Extract pagination info before formatting
		const pagination = extractPaginationInfo(
			commitsData,
			PaginationType.PAGE,
		);

		const formattedHistory = formatCommitHistory(commitsData, {
			revision: params.revision,
			path: params.path,
		});

		// Create the final content by combining the formatted commit history with pagination information
		let finalContent = formattedHistory;

		// Add pagination information if available
		if (
			pagination &&
			(pagination.hasMore || pagination.count !== undefined)
		) {
			const paginationString = formatPagination(pagination);
			finalContent += '\n\n' + paginationString;
		}

		return {
			content: finalContent,
		};
	} catch (error) {
		throw handleControllerError(error, {
			entityType: 'Commit History',
			operation: 'retrieving',
			source: 'controllers/atlassian.repositories.commit.controller.ts@handleCommitHistory',
			additionalInfo: { options },
		});
	}
}

```

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

```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logger } from '../utils/logger.util.js';
import {
	SearchToolArgsSchema,
	type SearchToolArgsType,
} from './atlassian.search.types.js';
import atlassianSearchController from '../controllers/atlassian.search.controller.js';
import { formatErrorForMcpTool } from '../utils/error.util.js';
import { getDefaultWorkspace } from '../utils/workspace.util.js';

// Set up logger
const logger = Logger.forContext('tools/atlassian.search.tool.ts');

/**
 * Handle search command in MCP
 */
async function handleSearch(args: Record<string, unknown>) {
	// Create a method-scoped logger
	const methodLogger = logger.forMethod('handleSearch');

	try {
		methodLogger.debug('Search tool called with args:', args);

		// Handle workspace similar to CLI implementation
		let workspace = args.workspaceSlug;
		if (!workspace) {
			const defaultWorkspace = await getDefaultWorkspace();
			if (!defaultWorkspace) {
				return {
					content: [
						{
							type: 'text' as const,
							text: 'Error: No workspace provided and no default workspace configured',
						},
					],
				};
			}
			workspace = defaultWorkspace;
			methodLogger.debug(`Using default workspace: ${workspace}`);
		}

		// Pass args to controller with workspace added
		const searchArgs: SearchToolArgsType = {
			workspaceSlug: workspace as string,
			repoSlug: args.repoSlug as string | undefined,
			query: args.query as string,
			scope:
				(args.scope as
					| 'code'
					| 'content'
					| 'repositories'
					| 'pullrequests') || 'code',
			contentType: args.contentType as string | undefined,
			language: args.language as string | undefined,
			extension: args.extension as string | undefined,
			limit: args.limit as number | undefined,
			cursor: args.cursor as string | undefined,
		};

		// Call the controller with proper parameter mapping
		const controllerOptions = {
			workspace: searchArgs.workspaceSlug,
			repo: searchArgs.repoSlug,
			query: searchArgs.query,
			type: searchArgs.scope,
			contentType: searchArgs.contentType,
			language: searchArgs.language,
			extension: searchArgs.extension,
			limit: searchArgs.limit,
			cursor: searchArgs.cursor,
		};

		const result = await atlassianSearchController.search(
			controllerOptions as Parameters<
				typeof atlassianSearchController.search
			>[0],
		);

		// Return the result content in MCP format
		return {
			content: [{ type: 'text' as const, text: result.content }],
		};
	} catch (error) {
		// Log the error
		methodLogger.error('Search tool failed:', error);

		// Format the error for MCP response
		return formatErrorForMcpTool(error);
	}
}

/**
 * Register the search tools with the MCP server
 */
function registerTools(server: McpServer) {
	// Register the search tool using the schema shape
	server.tool(
		'bb_search',
		'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.',
		SearchToolArgsSchema.shape,
		handleSearch,
	);

	logger.debug('Successfully registered Bitbucket search tools');
}

export default { registerTools };

```

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

```typescript
import { z } from 'zod';

/**
 * Schema for the branch diff tool arguments
 */
export const BranchDiffArgsSchema = z.object({
	/**
	 * Workspace slug containing the repository
	 */
	workspaceSlug: z
		.string()
		.optional()
		.describe(
			'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"',
		),

	/**
	 * Repository slug containing the branches
	 */
	repoSlug: z
		.string()
		.min(1, 'Repository slug is required')
		.describe(
			'Repository slug containing the branches. Must be a valid repository slug in the specified workspace. Example: "project-api"',
		),

	/**
	 * Source branch (feature branch)
	 */
	sourceBranch: z
		.string()
		.min(1, 'Source branch is required')
		.describe(
			'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"',
		),

	/**
	 * Destination branch (target branch like main/master)
	 */
	destinationBranch: z
		.string()
		.optional()
		.describe(
			'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"',
		),

	/**
	 * Include full diff in the output
	 */
	includeFullDiff: z
		.boolean()
		.optional()
		.describe(
			'Whether to include the full code diff in the output. Defaults to true for rich output.',
		),

	/**
	 * Maximum number of files to return per page
	 */
	limit: z
		.number()
		.int()
		.positive()
		.optional()
		.describe('Maximum number of changed files to return in results'),

	/**
	 * Pagination cursor for retrieving additional results
	 */
	cursor: z
		.number()
		.int()
		.positive()
		.optional()
		.describe('Pagination cursor for retrieving additional results'),
});

export type BranchDiffArgsType = z.infer<typeof BranchDiffArgsSchema>;

/**
 * Schema for the commit diff tool arguments
 */
export const CommitDiffArgsSchema = z.object({
	workspaceSlug: z
		.string()
		.optional()
		.describe(
			'Workspace slug containing the repository. If not provided, the system will use your default workspace.',
		),
	repoSlug: z
		.string()
		.min(1)
		.describe('Repository slug to compare commits in'),
	sinceCommit: z
		.string()
		.min(1)
		.describe(
			'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.',
		),
	untilCommit: z
		.string()
		.min(1)
		.describe(
			'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.',
		),
	includeFullDiff: z
		.boolean()
		.optional()
		.describe(
			'Whether to include the full code diff in the response (default: false)',
		),
	limit: z
		.number()
		.int()
		.positive()
		.optional()
		.describe('Maximum number of changed files to return in results'),
	cursor: z
		.number()
		.int()
		.positive()
		.optional()
		.describe('Pagination cursor for retrieving additional results'),
});

export type CommitDiffArgsType = z.infer<typeof CommitDiffArgsSchema>;

```

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

```typescript
import { ControllerResponse } from '../types/common.types.js';
import { CreatePullRequestParams } from '../services/vendor.atlassian.pullrequests.types.js';
import { CreatePullRequestToolArgsType } from '../tools/atlassian.pullrequests.types.js';
import {
	atlassianPullRequestsService,
	Logger,
	handleControllerError,
	formatPullRequestDetails,
	applyDefaults,
	optimizeBitbucketMarkdown,
	getDefaultWorkspace,
} from './atlassian.pullrequests.base.controller.js';

/**
 * Create a new pull request in Bitbucket
 * @param options - Options including workspace slug, repo slug, source branch, target branch, title, etc.
 * @returns Promise with formatted pull request details as Markdown content
 */
async function add(
	options: CreatePullRequestToolArgsType,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.pullrequests.create.controller.ts',
		'add',
	);

	try {
		// Apply defaults if needed (none for this operation)
		const mergedOptions = applyDefaults<CreatePullRequestToolArgsType>(
			options,
			{},
		);

		// Handle optional workspaceSlug - get default if not provided
		if (!mergedOptions.workspaceSlug) {
			methodLogger.debug(
				'No workspace provided, fetching default workspace',
			);
			const defaultWorkspace = await getDefaultWorkspace();
			if (!defaultWorkspace) {
				throw new Error(
					'Could not determine a default workspace. Please provide a workspaceSlug.',
				);
			}
			mergedOptions.workspaceSlug = defaultWorkspace;
			methodLogger.debug(
				`Using default workspace: ${mergedOptions.workspaceSlug}`,
			);
		}

		const {
			workspaceSlug,
			repoSlug,
			title,
			sourceBranch,
			destinationBranch,
			description,
			closeSourceBranch,
		} = mergedOptions;

		// Validate required parameters
		if (
			!workspaceSlug ||
			!repoSlug ||
			!title ||
			!sourceBranch ||
			!destinationBranch
		) {
			throw new Error(
				'Workspace slug, repository slug, title, source branch, and destination branch are required',
			);
		}

		methodLogger.debug(
			`Creating PR in ${workspaceSlug}/${repoSlug} from ${sourceBranch} to ${destinationBranch}`,
			{
				title,
				descriptionLength: description?.length,
				closeSourceBranch,
			},
		);

		// Process description - optimize Markdown if provided
		const optimizedDescription = description
			? optimizeBitbucketMarkdown(description)
			: undefined;

		// Map controller options to service parameters
		const serviceParams: CreatePullRequestParams = {
			workspace: workspaceSlug,
			repo_slug: repoSlug,
			title,
			source: {
				branch: {
					name: sourceBranch,
				},
			},
			destination: {
				branch: {
					name: destinationBranch,
				},
			},
			description: optimizedDescription,
			close_source_branch: closeSourceBranch,
		};

		// Create the pull request through the service
		const pullRequestResult =
			await atlassianPullRequestsService.create(serviceParams);

		methodLogger.debug('Pull request created successfully', {
			id: pullRequestResult.id,
			title: pullRequestResult.title,
			sourceBranch,
			destinationBranch,
		});

		// Format the pull request details using the formatter
		const formattedContent = formatPullRequestDetails(pullRequestResult);

		// Return formatted content with success message
		return {
			content: `## Pull Request Created Successfully\n\n${formattedContent}`,
		};
	} catch (error) {
		// Use the standardized error handler
		throw handleControllerError(error, {
			entityType: 'Pull Request',
			operation: 'creating',
			source: 'controllers/atlassian.pullrequests.create.controller.ts@add',
			additionalInfo: { options },
		});
	}
}

// Export the controller functions
export default { add };

```

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

```markdown
# MCP Server Style Guide

Based on the patterns observed and best practices, I recommend adopting the following consistent style guide across all your MCP servers:

| Element              | Convention                                                                                                                                    | Rationale / Examples                                                                                                                              |
| :------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ |
| **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`                                                                                            |
| **CLI Options**      | `--kebab-case`. Be specific (e.g., `--workspace-slug`, not just `--slug`).                                                                    | `--project-key-or-id`, `--source-branch`                                                                                                          |
| **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`. |
| **MCP Arguments**    | `camelCase`. Suffix identifiers consistently (e.g., `Id`, `Key`, `Slug`). Avoid abbreviations unless universal.                               | `workspaceSlug`, `pullRequestId`, `sourceBranch`, `pageId`.                                                                                       |
| **Boolean Args**     | Use verb prefixes for clarity (`includeXxx`, `launchBrowser`). Avoid bare adjectives (`--https`).                                             | `includeExtendedData: boolean`, `launchBrowser: boolean`                                                                                          |
| **Array Args**       | Use plural names (`spaceIds`, `labels`, `statuses`).                                                                                          | `spaceIds: string[]`, `labels: string[]`                                                                                                          |
| **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.`                            |
| **Arg Descriptions** | Start lowercase, explain purpose clearly. Mention defaults or constraints.                                                                    | `numeric ID of the page to retrieve (e.g., "456789"). Required.`                                                                                  |
| **ID/Key Naming**    | Use consistent suffixes like `Id`, `Key`, `Slug`, `KeyOrId` where appropriate.                                                                | `pageId`, `projectKeyOrId`, `workspaceSlug`                                                                                                       |

Adopting this guide will make the tools more predictable and easier for both humans and AI agents to understand and use correctly.

```

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

```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logger } from '../utils/logger.util.js';
import { formatErrorForMcpTool } from '../utils/error.util.js';
import {
	ListWorkspacesToolArgs,
	type ListWorkspacesToolArgsType,
	type GetWorkspaceToolArgsType,
	GetWorkspaceToolArgs,
} from './atlassian.workspaces.types.js';

import atlassianWorkspacesController from '../controllers/atlassian.workspaces.controller.js';

// Create a contextualized logger for this file
const toolLogger = Logger.forContext('tools/atlassian.workspaces.tool.ts');

// Log tool initialization
toolLogger.debug('Bitbucket workspaces tool initialized');

/**
 * MCP Tool: List Bitbucket Workspaces
 *
 * Lists Bitbucket workspaces available to the authenticated user with optional filtering.
 * Returns a formatted markdown response with workspace details.
 *
 * @param args - Tool arguments for filtering workspaces
 * @returns MCP response with formatted workspaces list
 * @throws Will return error message if workspace listing fails
 */
async function listWorkspaces(args: Record<string, unknown>) {
	const methodLogger = Logger.forContext(
		'tools/atlassian.workspaces.tool.ts',
		'listWorkspaces',
	);
	methodLogger.debug('Listing Bitbucket workspaces with filters:', args);

	try {
		// Pass args directly to controller without any logic
		const result = await atlassianWorkspacesController.list(
			args as ListWorkspacesToolArgsType,
		);

		methodLogger.debug('Successfully retrieved workspaces from controller');

		return {
			content: [
				{
					type: 'text' as const,
					text: result.content,
				},
			],
		};
	} catch (error) {
		methodLogger.error('Failed to list workspaces', error);
		return formatErrorForMcpTool(error);
	}
}

/**
 * MCP Tool: Get Bitbucket Workspace Details
 *
 * Retrieves detailed information about a specific Bitbucket workspace.
 * Returns a formatted markdown response with workspace metadata.
 *
 * @param args - Tool arguments containing the workspace slug
 * @returns MCP response with formatted workspace details
 * @throws Will return error message if workspace retrieval fails
 */
async function getWorkspace(args: Record<string, unknown>) {
	const methodLogger = Logger.forContext(
		'tools/atlassian.workspaces.tool.ts',
		'getWorkspace',
	);
	methodLogger.debug('Getting workspace details:', args);

	try {
		// Pass args directly to controller without any logic
		const result = await atlassianWorkspacesController.get(
			args as GetWorkspaceToolArgsType,
		);

		methodLogger.debug(
			'Successfully retrieved workspace details from controller',
		);

		return {
			content: [
				{
					type: 'text' as const,
					text: result.content,
				},
			],
		};
	} catch (error) {
		methodLogger.error('Failed to get workspace details', error);
		return formatErrorForMcpTool(error);
	}
}

/**
 * Register all Bitbucket workspace tools with the MCP server.
 */
function registerTools(server: McpServer) {
	const registerLogger = Logger.forContext(
		'tools/atlassian.workspaces.tool.ts',
		'registerTools',
	);
	registerLogger.debug('Registering Workspace tools...');

	// Register the list workspaces tool
	server.tool(
		'bb_ls_workspaces',
		`Lists workspaces within your Bitbucket account. Returns a formatted Markdown list showing workspace slugs, names, and membership role. Requires Bitbucket credentials to be configured.`,
		ListWorkspacesToolArgs.shape,
		listWorkspaces,
	);

	// Register the get workspace details tool
	server.tool(
		'bb_get_workspace',
		`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.`,
		GetWorkspaceToolArgs.shape,
		getWorkspace,
	);

	registerLogger.debug('Successfully registered Workspace tools');
}

export default { registerTools };

```

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

```typescript
import { Logger } from '../utils/logger.util.js';
import { ContentType } from '../utils/atlassian.util.js';
import { handleCodeSearch } from './atlassian.search.code.controller.js';
import { handleContentSearch } from './atlassian.search.content.controller.js';
import { handleRepositorySearch } from './atlassian.search.repositories.controller.js';
import { handlePullRequestSearch } from './atlassian.search.pullrequests.controller.js';
import { DEFAULT_PAGE_SIZE, applyDefaults } from '../utils/defaults.util.js';
import { ControllerResponse } from '../types/common.types.js';
import { handleControllerError } from '../utils/error-handler.util.js';

// Logger instance for this module
const logger = Logger.forContext('controllers/atlassian.search.controller.ts');

/**
 * Search interface options
 */
export interface SearchOptions {
	/** The workspace to search in */
	workspace?: string;
	/** The repository to search in (optional) */
	repo?: string;
	/** The search query */
	query?: string;
	/** The type of search to perform */
	type?: string;
	/** The content type to filter by (for content search) */
	contentType?: string;
	/** The language to filter by (for code search) */
	language?: string;
	/** File extension to filter by (for code search) */
	extension?: string;
	/** Maximum number of results to return */
	limit?: number;
	/** Pagination cursor */
	cursor?: string;
}

/**
 * Perform a search across various Bitbucket data types
 *
 * @param options Search options
 * @returns Formatted search results
 */
async function search(
	options: SearchOptions = {},
): Promise<ControllerResponse> {
	const methodLogger = logger.forMethod('search');

	try {
		// Apply default values
		const defaults: Partial<SearchOptions> = {
			type: 'code',
			workspace: '',
			limit: DEFAULT_PAGE_SIZE,
		};
		const params = applyDefaults<SearchOptions>(options, defaults);
		methodLogger.debug('Search options (with defaults):', params);

		// Validate parameters
		if (!params.workspace) {
			methodLogger.warn('No workspace provided for search');
			return {
				content: 'Error: Please provide a workspace to search in.',
			};
		}

		// Convert content type string to enum if provided (outside the switch statement)
		let contentTypeEnum: ContentType | undefined = undefined;
		if (params.contentType) {
			contentTypeEnum = params.contentType.toLowerCase() as ContentType;
		}

		// Dispatch to the appropriate search function based on type
		switch (params.type?.toLowerCase()) {
			case 'code':
				return await handleCodeSearch(
					params.workspace,
					params.repo,
					params.query,
					params.limit,
					params.cursor,
					params.language,
					params.extension,
				);

			case 'content':
				return await handleContentSearch(
					params.workspace,
					params.repo,
					params.query,
					params.limit,
					params.cursor,
					contentTypeEnum,
				);

			case 'repos':
			case 'repositories':
				return await handleRepositorySearch(
					params.workspace,
					params.repo,
					params.query,
					params.limit,
					params.cursor,
				);

			case 'prs':
			case 'pullrequests':
				if (!params.repo) {
					return {
						content:
							'Error: Repository is required for pull request search.',
					};
				}
				return await handlePullRequestSearch(
					params.workspace,
					params.repo,
					params.query,
					params.limit,
					params.cursor,
				);

			default:
				methodLogger.warn(`Unknown search type: ${params.type}`);
				return {
					content: `Error: Unknown search type "${params.type}". Supported types are: code, content, repositories, pullrequests.`,
				};
		}
	} catch (error) {
		// Pass the error to the handler with context
		throw handleControllerError(error, {
			entityType: 'Search',
			operation: 'search',
			source: 'controllers/atlassian.search.controller.ts@search',
			additionalInfo: options as Record<string, unknown>,
		});
	}
}

export default { search };

```

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

```typescript
import {
	ErrorType,
	McpError,
	createApiError,
	createAuthMissingError,
	createAuthInvalidError,
	createUnexpectedError,
	ensureMcpError,
	formatErrorForMcpTool,
	formatErrorForMcpResource,
} from './error.util.js';

describe('Error Utility', () => {
	describe('McpError', () => {
		it('should create an error with the correct properties', () => {
			const error = new McpError('Test error', ErrorType.API_ERROR, 404);

			expect(error).toBeInstanceOf(Error);
			expect(error).toBeInstanceOf(McpError);
			expect(error.message).toBe('Test error');
			expect(error.type).toBe(ErrorType.API_ERROR);
			expect(error.statusCode).toBe(404);
			expect(error.name).toBe('McpError');
		});
	});

	describe('Error Factory Functions', () => {
		it('should create auth missing error', () => {
			const error = createAuthMissingError();

			expect(error).toBeInstanceOf(McpError);
			expect(error.type).toBe(ErrorType.AUTH_MISSING);
			expect(error.message).toBe(
				'Authentication credentials are missing',
			);
		});

		it('should create auth invalid error', () => {
			const error = createAuthInvalidError('Invalid token');

			expect(error).toBeInstanceOf(McpError);
			expect(error.type).toBe(ErrorType.AUTH_INVALID);
			expect(error.statusCode).toBe(401);
			expect(error.message).toBe('Invalid token');
		});

		it('should create API error', () => {
			const originalError = new Error('Original error');
			const error = createApiError('API failed', 500, originalError);

			expect(error).toBeInstanceOf(McpError);
			expect(error.type).toBe(ErrorType.API_ERROR);
			expect(error.statusCode).toBe(500);
			expect(error.message).toBe('API failed');
			expect(error.originalError).toBe(originalError);
		});

		it('should create unexpected error', () => {
			const error = createUnexpectedError();

			expect(error).toBeInstanceOf(McpError);
			expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR);
			expect(error.message).toBe('An unexpected error occurred');
		});
	});

	describe('ensureMcpError', () => {
		it('should return the same error if it is already an McpError', () => {
			const originalError = createApiError('Original error');
			const error = ensureMcpError(originalError);

			expect(error).toBe(originalError);
		});

		it('should wrap a standard Error', () => {
			const originalError = new Error('Standard error');
			const error = ensureMcpError(originalError);

			expect(error).toBeInstanceOf(McpError);
			expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR);
			expect(error.message).toBe('Standard error');
			expect(error.originalError).toBe(originalError);
		});

		it('should handle non-Error objects', () => {
			const error = ensureMcpError('String error');

			expect(error).toBeInstanceOf(McpError);
			expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR);
			expect(error.message).toBe('String error');
		});
	});

	describe('formatErrorForMcpTool', () => {
		it('should format an error for MCP tool response', () => {
			const error = createApiError('API error');
			const response = formatErrorForMcpTool(error);

			expect(response).toHaveProperty('content');
			expect(response.content).toHaveLength(1);
			expect(response.content[0]).toHaveProperty('type', 'text');
			expect(response.content[0]).toHaveProperty(
				'text',
				'Error: API error',
			);
		});
	});

	describe('formatErrorForMcpResource', () => {
		it('should format an error for MCP resource response', () => {
			const error = createApiError('API error');
			const response = formatErrorForMcpResource(error, 'test://uri');

			expect(response).toHaveProperty('contents');
			expect(response.contents).toHaveLength(1);
			expect(response.contents[0]).toHaveProperty('uri', 'test://uri');
			expect(response.contents[0]).toHaveProperty(
				'text',
				'Error: API error',
			);
			expect(response.contents[0]).toHaveProperty(
				'mimeType',
				'text/plain',
			);
			expect(response.contents[0]).toHaveProperty(
				'description',
				'Error: API_ERROR',
			);
		});
	});
});

```

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

```typescript
import { z } from 'zod';

/**
 * Pagination arguments
 * Used for pagination of search results
 */
const PaginationArgs = z.object({
	/**
	 * Maximum number of items to return (1-100).
	 * Use this to control the response size.
	 * Useful for pagination or when you only need a few results.
	 */
	limit: z
		.number()
		.min(1)
		.max(100)
		.optional()
		.describe(
			'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.',
		),

	/**
	 * 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.
	 */
	cursor: z
		.string()
		.optional()
		.describe(
			'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.',
		),
});

/**
 * Bitbucket search tool arguments schema base
 */
export const SearchToolArgsBase = z
	.object({
		/**
		 * Workspace slug to search in. Example: "myteam"
		 * This maps to the CLI's "--workspace" parameter.
		 */
		workspaceSlug: z
			.string()
			.optional()
			.describe(
				'Workspace slug to search in. If not provided, the system will use your default workspace. Example: "myteam". Equivalent to --workspace in CLI.',
			),

		/**
		 * Optional: Repository slug to limit search scope. Required for `pullrequests` scope. Example: "project-api"
		 * This maps to the CLI's "--repo" parameter.
		 */
		repoSlug: z
			.string()
			.optional()
			.describe(
				'Optional: Repository slug to limit search scope. Required for `pullrequests` scope. Example: "project-api". Equivalent to --repo in CLI.',
			),

		/**
		 * Search query text. Required. Will match against content based on the selected search scope.
		 * This maps to the CLI's "--query" parameter.
		 */
		query: z
			.string()
			.min(1)
			.describe(
				'Search query text. Required. Will match against content based on the selected search scope. Equivalent to --query in CLI.',
			),

		/**
		 * Search scope: "code", "content", "repositories", "pullrequests". Default: "code"
		 * This maps to the CLI's "--type" parameter.
		 */
		scope: z
			.enum(['code', 'content', 'repositories', 'pullrequests'])
			.optional()
			.default('code')
			.describe(
				'Search scope: "code", "content", "repositories", "pullrequests". Default: "code". Equivalent to --type in CLI.',
			),

		/**
		 * Content type for content search (e.g., "wiki", "issue")
		 * This maps to the CLI's "--content-type" parameter.
		 */
		contentType: z
			.string()
			.optional()
			.describe(
				'Content type for content search (e.g., "wiki", "issue"). Equivalent to --content-type in CLI.',
			),

		/**
		 * Filter code search by language.
		 * This maps to the CLI's "--language" parameter.
		 */
		language: z
			.string()
			.optional()
			.describe(
				'Filter code search by language. Equivalent to --language in CLI.',
			),

		/**
		 * Filter code search by file extension.
		 * This maps to the CLI's "--extension" parameter.
		 */
		extension: z
			.string()
			.optional()
			.describe(
				'Filter code search by file extension. Equivalent to --extension in CLI.',
			),
	})
	.merge(PaginationArgs);

/**
 * Bitbucket search tool arguments schema with validation
 */
export const SearchToolArgs = SearchToolArgsBase.superRefine((data, ctx) => {
	// Make repoSlug required when scope is 'pullrequests'
	if (data.scope === 'pullrequests' && !data.repoSlug) {
		ctx.addIssue({
			code: z.ZodIssueCode.custom,
			message: 'repoSlug is required when scope is "pullrequests"',
			path: ['repoSlug'],
		});
	}
});

// Export both the schema and its shape for use with the MCP server
export const SearchToolArgsSchema = SearchToolArgsBase;

export type SearchToolArgsType = z.infer<typeof SearchToolArgs>;

```

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

```typescript
import { ControllerResponse } from '../types/common.types.js';
import { ListPullRequestsParams } from '../services/vendor.atlassian.pullrequests.types.js';
import { ListPullRequestsToolArgsType } from '../tools/atlassian.pullrequests.types.js';
import {
	atlassianPullRequestsService,
	Logger,
	handleControllerError,
	extractPaginationInfo,
	PaginationType,
	formatPagination,
	formatPullRequestsList,
	DEFAULT_PAGE_SIZE,
	applyDefaults,
	getDefaultWorkspace,
} from './atlassian.pullrequests.base.controller.js';

/**
 * List Bitbucket pull requests with optional filtering options
 * @param options - Options for listing pull requests including workspace slug and repo slug
 * @returns Promise with formatted pull requests list content and pagination information
 */
async function list(
	options: ListPullRequestsToolArgsType,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.pullrequests.list.controller.ts',
		'list',
	);

	try {
		// Create defaults object with proper typing
		const defaults: Partial<ListPullRequestsToolArgsType> = {
			limit: DEFAULT_PAGE_SIZE,
		};

		// Apply defaults
		const mergedOptions = applyDefaults<ListPullRequestsToolArgsType>(
			options,
			defaults,
		);

		// Handle optional workspaceSlug - get default if not provided
		if (!mergedOptions.workspaceSlug) {
			methodLogger.debug(
				'No workspace provided, fetching default workspace',
			);
			const defaultWorkspace = await getDefaultWorkspace();
			if (!defaultWorkspace) {
				throw new Error(
					'Could not determine a default workspace. Please provide a workspaceSlug.',
				);
			}
			mergedOptions.workspaceSlug = defaultWorkspace;
			methodLogger.debug(
				`Using default workspace: ${mergedOptions.workspaceSlug}`,
			);
		}

		const { workspaceSlug, repoSlug } = mergedOptions;

		if (!workspaceSlug || !repoSlug) {
			throw new Error('Workspace slug and repository slug are required');
		}

		methodLogger.debug(
			`Listing pull requests for ${workspaceSlug}/${repoSlug}...`,
			mergedOptions,
		);

		// Format the query for Bitbucket API if provided - specifically target title/description
		const formattedQuery = mergedOptions.query
			? `(title ~ "${mergedOptions.query}" OR description ~ "${mergedOptions.query}")` // Construct specific query for PRs
			: undefined;

		// Map controller options to service parameters
		const serviceParams: ListPullRequestsParams = {
			workspace: workspaceSlug,
			repo_slug: repoSlug,
			pagelen: mergedOptions.limit,
			page: mergedOptions.cursor
				? parseInt(mergedOptions.cursor, 10)
				: undefined,
			state: mergedOptions.state,
			sort: '-updated_on', // Sort by most recently updated first
			...(formattedQuery && { q: formattedQuery }),
		};

		methodLogger.debug('Using service parameters:', serviceParams);

		const pullRequestsData =
			await atlassianPullRequestsService.list(serviceParams);

		methodLogger.debug(
			`Retrieved ${pullRequestsData.values?.length || 0} pull requests`,
		);

		// Extract pagination information using the utility
		const pagination = extractPaginationInfo(
			pullRequestsData,
			PaginationType.PAGE,
		);

		// Format the pull requests data for display using the formatter
		const formattedPullRequests = formatPullRequestsList(pullRequestsData);

		// Create the final content by combining the formatted pull requests with pagination information
		let finalContent = formattedPullRequests;

		// Add pagination information if available
		if (
			pagination &&
			(pagination.hasMore || pagination.count !== undefined)
		) {
			const paginationString = formatPagination(pagination);
			finalContent += '\n\n' + paginationString;
		}

		return {
			content: finalContent,
		};
	} catch (error) {
		// Use the standardized error handler
		throw handleControllerError(error, {
			entityType: 'Pull Requests',
			operation: 'listing',
			source: 'controllers/atlassian.pullrequests.list.controller.ts@list',
			additionalInfo: { options },
		});
	}
}

// Export the controller functions
export default { list };

```

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

```typescript
import { z } from 'zod';
import { Logger } from '../utils/logger.util.js';
import { NETWORK_TIMEOUTS } from '../utils/constants.util.js';
import {
	fetchAtlassian,
	getAtlassianCredentials,
} from '../utils/transport.util.js';
import {
	createApiError,
	createAuthMissingError,
	McpError,
} from '../utils/error.util.js';
import {
	GetDiffstatParamsSchema,
	DiffstatResponseSchema,
	GetRawDiffParamsSchema,
	type GetDiffstatParams,
	type DiffstatResponse,
	type GetRawDiffParams,
} from './vendor.atlassian.repositories.diff.types.js';

/**
 * Base API path for Bitbucket REST API v2
 */
const API_PATH = '/2.0';

const serviceLogger = Logger.forContext(
	'services/vendor.atlassian.repositories.diff.service.ts',
);
serviceLogger.debug('Bitbucket diff service initialised');

/**
 * Retrieve diffstat (per–file summary) between two refs (branches, tags or commits).
 * Follows Bitbucket Cloud endpoint:
 *   GET /2.0/repositories/{workspace}/{repo_slug}/diffstat/{spec}
 */
export async function getDiffstat(
	params: GetDiffstatParams,
): Promise<DiffstatResponse> {
	const methodLogger = serviceLogger.forMethod('getDiffstat');
	methodLogger.debug('Fetching diffstat with params', params);

	// Validate params
	try {
		GetDiffstatParamsSchema.parse(params);
	} catch (err) {
		if (err instanceof z.ZodError) {
			throw createApiError(
				`Invalid parameters: ${err.issues.map((e) => e.message).join(', ')}`,
				400,
				err,
			);
		}
		throw err;
	}

	const credentials = getAtlassianCredentials();
	if (!credentials) {
		throw createAuthMissingError('Atlassian credentials are required');
	}

	const query = new URLSearchParams();
	if (params.pagelen) query.set('pagelen', String(params.pagelen));
	if (params.cursor) query.set('page', String(params.cursor));
	if (params.topic !== undefined) query.set('topic', String(params.topic));

	const queryString = query.toString() ? `?${query.toString()}` : '';
	const encodedSpec = encodeURIComponent(params.spec);
	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/diffstat/${encodedSpec}${queryString}`;

	methodLogger.debug(`Requesting: ${path}`);
	try {
		const rawData = await fetchAtlassian(credentials, path);
		try {
			const validated = DiffstatResponseSchema.parse(rawData);
			return validated;
		} catch (error) {
			if (error instanceof z.ZodError) {
				methodLogger.error(
					'Bitbucket API response validation failed:',
					error.format(),
				);
				throw createApiError(
					`Invalid response format from Bitbucket API for diffstat: ${error.message}`,
					500,
					error,
				);
			}
			throw error; // Re-throw any other errors
		}
	} catch (error) {
		if (error instanceof McpError) throw error;
		throw createApiError(
			`Failed to fetch diffstat: ${error instanceof Error ? error.message : String(error)}`,
			500,
			error,
		);
	}
}

/**
 * Retrieve raw unified diff between two refs.
 * Endpoint: /diff/{spec}
 */
export async function getRawDiff(params: GetRawDiffParams): Promise<string> {
	const methodLogger = serviceLogger.forMethod('getRawDiff');
	methodLogger.debug('Fetching raw diff', params);
	try {
		GetRawDiffParamsSchema.parse(params);
	} catch (err) {
		if (err instanceof z.ZodError) {
			throw createApiError(
				`Invalid parameters: ${err.issues.map((e) => e.message).join(', ')}`,
				400,
				err,
			);
		}
		throw err;
	}
	const credentials = getAtlassianCredentials();
	if (!credentials) {
		throw createAuthMissingError('Atlassian credentials are required');
	}

	const encodedSpec = encodeURIComponent(params.spec);
	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/diff/${encodedSpec}`;
	methodLogger.debug(`Requesting: ${path}`);
	try {
		// fetchAtlassian will return string for text/plain
		const diffText = await fetchAtlassian<string>(credentials, path, {
			timeout: NETWORK_TIMEOUTS.LARGE_REQUEST_TIMEOUT,
		});
		return diffText;
	} catch (error) {
		if (error instanceof McpError) throw error;
		throw createApiError(
			`Failed to fetch raw diff: ${error instanceof Error ? error.message : String(error)}`,
			500,
			error,
		);
	}
}

```

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

```typescript
import { adfToMarkdown } from './adf.util.js';

describe('ADF Utility', () => {
	describe('adfToMarkdown', () => {
		it('should handle empty or undefined input', () => {
			expect(adfToMarkdown(null)).toBe('');
			expect(adfToMarkdown(undefined)).toBe('');
			expect(adfToMarkdown('')).toBe('');
		});

		it('should handle non-ADF string input', () => {
			expect(adfToMarkdown('plain text')).toBe('plain text');
		});

		it('should convert basic paragraph', () => {
			const adf = {
				type: 'doc',
				version: 1,
				content: [
					{
						type: 'paragraph',
						content: [
							{
								type: 'text',
								text: 'This is a paragraph',
							},
						],
					},
				],
			};

			expect(adfToMarkdown(adf)).toBe('This is a paragraph');
		});

		it('should convert multiple paragraphs', () => {
			const adf = {
				type: 'doc',
				version: 1,
				content: [
					{
						type: 'paragraph',
						content: [
							{
								type: 'text',
								text: 'First paragraph',
							},
						],
					},
					{
						type: 'paragraph',
						content: [
							{
								type: 'text',
								text: 'Second paragraph',
							},
						],
					},
				],
			};

			expect(adfToMarkdown(adf)).toBe(
				'First paragraph\n\nSecond paragraph',
			);
		});

		it('should convert headings', () => {
			const adf = {
				type: 'doc',
				version: 1,
				content: [
					{
						type: 'heading',
						attrs: { level: 1 },
						content: [
							{
								type: 'text',
								text: 'Heading 1',
							},
						],
					},
					{
						type: 'heading',
						attrs: { level: 2 },
						content: [
							{
								type: 'text',
								text: 'Heading 2',
							},
						],
					},
				],
			};

			expect(adfToMarkdown(adf)).toBe('# Heading 1\n\n## Heading 2');
		});

		it('should convert text with marks', () => {
			const adf = {
				type: 'doc',
				version: 1,
				content: [
					{
						type: 'paragraph',
						content: [
							{
								type: 'text',
								text: 'Bold',
								marks: [{ type: 'strong' }],
							},
							{
								type: 'text',
								text: ' and ',
							},
							{
								type: 'text',
								text: 'italic',
								marks: [{ type: 'em' }],
							},
							{
								type: 'text',
								text: ' and ',
							},
							{
								type: 'text',
								text: 'code',
								marks: [{ type: 'code' }],
							},
						],
					},
				],
			};

			expect(adfToMarkdown(adf)).toBe('**Bold** and *italic* and `code`');
		});

		it('should convert bullet lists', () => {
			const adf = {
				type: 'doc',
				version: 1,
				content: [
					{
						type: 'bulletList',
						content: [
							{
								type: 'listItem',
								content: [
									{
										type: 'paragraph',
										content: [
											{
												type: 'text',
												text: 'Item 1',
											},
										],
									},
								],
							},
							{
								type: 'listItem',
								content: [
									{
										type: 'paragraph',
										content: [
											{
												type: 'text',
												text: 'Item 2',
											},
										],
									},
								],
							},
						],
					},
				],
			};

			expect(adfToMarkdown(adf)).toBe('- Item 1\n- Item 2');
		});

		it('should convert code blocks', () => {
			const adf = {
				type: 'doc',
				version: 1,
				content: [
					{
						type: 'codeBlock',
						attrs: { language: 'javascript' },
						content: [
							{
								type: 'text',
								text: 'const x = 1;',
							},
						],
					},
				],
			};

			expect(adfToMarkdown(adf)).toBe('```javascript\nconst x = 1;\n```');
		});

		it('should convert links', () => {
			const adf = {
				type: 'doc',
				version: 1,
				content: [
					{
						type: 'paragraph',
						content: [
							{
								type: 'text',
								text: 'Visit',
							},
							{
								type: 'text',
								text: ' Atlassian',
								marks: [
									{
										type: 'link',
										attrs: {
											href: 'https://atlassian.com',
										},
									},
								],
							},
						],
					},
				],
			};

			expect(adfToMarkdown(adf)).toBe(
				'Visit[ Atlassian](https://atlassian.com)',
			);
		});
	});
});

```

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

```typescript
import atlassianPullRequestsService from '../services/vendor.atlassian.pullrequests.service.js';
import { Logger } from '../utils/logger.util.js';
import { handleControllerError } from '../utils/error-handler.util.js';
import {
	extractPaginationInfo,
	PaginationType,
} from '../utils/pagination.util.js';
import { formatPagination } from '../utils/formatter.util.js';
import {
	formatPullRequestsList,
	formatPullRequestDetails,
	formatPullRequestComments,
} from './atlassian.pullrequests.formatter.js';
import {
	PullRequestComment,
	PullRequestCommentsResponse,
} from '../services/vendor.atlassian.pullrequests.types.js';
import { DEFAULT_PAGE_SIZE, applyDefaults } from '../utils/defaults.util.js';
import { extractDiffSnippet } from '../utils/diff.util.js';
import { optimizeBitbucketMarkdown } from '../utils/formatter.util.js';
import { getDefaultWorkspace } from '../utils/workspace.util.js';

/**
 * Base controller for managing Bitbucket pull requests.
 * Contains shared utilities and types used by the specific PR controller files.
 *
 * NOTE ON MARKDOWN HANDLING:
 * Unlike Jira (which uses ADF) or Confluence (which uses a mix of formats),
 * Bitbucket Cloud API natively accepts Markdown for text content in both directions:
 * - When sending data TO the API (comments, PR descriptions)
 * - When receiving data FROM the API (PR descriptions, comments)
 *
 * The API expects content in the format: { content: { raw: "markdown-text" } }
 *
 * We use optimizeBitbucketMarkdown() to address specific rendering quirks in
 * Bitbucket's markdown renderer but it does NOT perform format conversion.
 * See formatter.util.ts for details on the specific issues it addresses.
 */

// Define an extended type for internal use within the controller/formatter
// to include the code snippet.
export interface PullRequestCommentWithSnippet extends PullRequestComment {
	codeSnippet?: string;
}

// Define a service-specific type for listing comments
export type ListCommentsParams = {
	workspace: string;
	repo_slug: string;
	pull_request_id: number;
	pagelen?: number;
	page?: number;
};

// Define a service-specific type for creating comments
export type CreateCommentParams = {
	workspace: string;
	repo_slug: string;
	pull_request_id: number;
	content: {
		raw: string;
	};
	inline?: {
		path: string;
		to?: number;
	};
	parent?: {
		id: number;
	};
};

// Helper function to enhance comments with code snippets
export async function enhanceCommentsWithSnippets(
	commentsData: PullRequestCommentsResponse,
	controllerMethodName: string, // To contextualize logs
): Promise<PullRequestCommentWithSnippet[]> {
	const methodLogger = Logger.forContext(
		`controllers/atlassian.pullrequests.base.controller.ts`,
		controllerMethodName, // Use provided method name for logger context
	);
	const commentsWithSnippets: PullRequestCommentWithSnippet[] = [];

	if (!commentsData.values || commentsData.values.length === 0) {
		return [];
	}

	for (const comment of commentsData.values) {
		let snippet = undefined;
		if (
			comment.inline &&
			comment.links?.code?.href &&
			comment.inline.to !== undefined
		) {
			try {
				methodLogger.debug(
					`Fetching diff for inline comment ${comment.id} from ${comment.links.code.href}`,
				);
				const diffContent =
					await atlassianPullRequestsService.getDiffForUrl(
						comment.links.code.href,
					);
				snippet = extractDiffSnippet(diffContent, comment.inline.to);
				methodLogger.debug(
					`Extracted snippet for comment ${comment.id} (length: ${snippet?.length})`,
				);
			} catch (snippetError) {
				methodLogger.warn(
					`Failed to fetch or parse snippet for comment ${comment.id}:`,
					snippetError,
				);
				// Continue without snippet if fetching/parsing fails
			}
		}
		commentsWithSnippets.push({ ...comment, codeSnippet: snippet });
	}
	return commentsWithSnippets;
}

export {
	atlassianPullRequestsService,
	Logger,
	handleControllerError,
	extractPaginationInfo,
	PaginationType,
	formatPagination,
	formatPullRequestsList,
	formatPullRequestDetails,
	formatPullRequestComments,
	DEFAULT_PAGE_SIZE,
	applyDefaults,
	optimizeBitbucketMarkdown,
	getDefaultWorkspace,
};

```

--------------------------------------------------------------------------------
/src/utils/bitbucket-error-detection.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, expect, test } from '@jest/globals';
import { detectErrorType, ErrorCode } from './error-handler.util.js';
import { createApiError } from './error.util.js';

describe('Bitbucket Error Detection', () => {
	describe('Classic Bitbucket error structure: { error: { message, detail } }', () => {
		test('detects not found errors', () => {
			// Create a mock Bitbucket error structure
			const bitbucketError = {
				error: {
					message: 'Repository not found',
					detail: 'The repository does not exist or you do not have access',
				},
			};
			const mcpError = createApiError('API Error', 404, bitbucketError);

			const result = detectErrorType(mcpError);
			expect(result).toEqual({
				code: ErrorCode.NOT_FOUND,
				statusCode: 404,
			});
		});

		test('detects access denied errors', () => {
			const bitbucketError = {
				error: {
					message: 'Access denied to this repository',
					detail: 'You need admin permissions to perform this action',
				},
			};
			const mcpError = createApiError('API Error', 403, bitbucketError);

			const result = detectErrorType(mcpError);
			expect(result).toEqual({
				code: ErrorCode.ACCESS_DENIED,
				statusCode: 403,
			});
		});

		test('detects validation errors', () => {
			const bitbucketError = {
				error: {
					message: 'Invalid parameter: repository name',
					detail: 'Repository name can only contain alphanumeric characters',
				},
			};
			const mcpError = createApiError('API Error', 400, bitbucketError);

			const result = detectErrorType(mcpError);
			expect(result).toEqual({
				code: ErrorCode.VALIDATION_ERROR,
				statusCode: 400,
			});
		});

		test('detects rate limit errors', () => {
			const bitbucketError = {
				error: {
					message: 'Too many requests',
					detail: 'Rate limit exceeded. Try again later.',
				},
			};
			const mcpError = createApiError('API Error', 429, bitbucketError);

			const result = detectErrorType(mcpError);
			expect(result).toEqual({
				code: ErrorCode.RATE_LIMIT_ERROR,
				statusCode: 429,
			});
		});
	});

	describe('Alternate Bitbucket error structure: { type: "error", ... }', () => {
		test('detects not found errors', () => {
			const altBitbucketError = {
				type: 'error',
				status: 404,
				message: 'Resource not found',
			};
			const mcpError = createApiError(
				'API Error',
				404,
				altBitbucketError,
			);

			const result = detectErrorType(mcpError);
			expect(result).toEqual({
				code: ErrorCode.NOT_FOUND,
				statusCode: 404,
			});
		});

		test('detects access denied errors', () => {
			const altBitbucketError = {
				type: 'error',
				status: 403,
				message: 'Forbidden',
			};
			const mcpError = createApiError(
				'API Error',
				403,
				altBitbucketError,
			);

			const result = detectErrorType(mcpError);
			expect(result).toEqual({
				code: ErrorCode.ACCESS_DENIED,
				statusCode: 403,
			});
		});
	});

	describe('Bitbucket errors array structure: { errors: [{ ... }] }', () => {
		test('detects errors from array structure', () => {
			const arrayBitbucketError = {
				errors: [
					{
						status: 400,
						code: 'INVALID_REQUEST_PARAMETER',
						title: 'Invalid parameter value',
						message: 'The parameter is not valid',
					},
				],
			};
			const mcpError = createApiError(
				'API Error',
				400,
				arrayBitbucketError,
			);

			const result = detectErrorType(mcpError);
			expect(result).toEqual({
				code: ErrorCode.VALIDATION_ERROR,
				statusCode: 400,
			});
		});
	});

	describe('Network errors in Bitbucket context', () => {
		test('detects network errors from TypeError', () => {
			const networkError = new TypeError('Failed to fetch');
			const mcpError = createApiError('Network Error', 500, networkError);

			const result = detectErrorType(mcpError);
			expect(result).toEqual({
				code: ErrorCode.NETWORK_ERROR,
				statusCode: 500,
			});
		});

		test('detects other common network error messages', () => {
			const errorMessages = [
				'network error occurred',
				'ECONNREFUSED',
				'ENOTFOUND',
				'Network request failed',
				'Failed to fetch',
			];

			errorMessages.forEach((msg) => {
				const error = new Error(msg);
				const result = detectErrorType(error);
				expect(result).toEqual({
					code: ErrorCode.NETWORK_ERROR,
					statusCode: 500,
				});
			});
		});
	});
});

```

--------------------------------------------------------------------------------
/src/controllers/atlassian.workspaces.formatter.ts:
--------------------------------------------------------------------------------

```typescript
import {
	WorkspaceDetailed,
	WorkspacePermissionsResponse,
	WorkspaceMembership,
} from '../services/vendor.atlassian.workspaces.types.js';
import {
	formatUrl,
	formatHeading,
	formatBulletList,
	formatSeparator,
	formatNumberedList,
	formatDate,
} from '../utils/formatter.util.js';

/**
 * Format a list of workspaces for display
 * @param workspacesData - Raw workspaces data from the API
 * @returns Formatted string with workspaces information in markdown format
 */
export function formatWorkspacesList(
	workspacesData: WorkspacePermissionsResponse,
): string {
	const workspaces = workspacesData.values || [];

	if (workspaces.length === 0) {
		return 'No workspaces found matching your criteria.';
	}

	const lines: string[] = [formatHeading('Bitbucket Workspaces', 1), ''];

	// Format each workspace with its details
	const formattedList = formatNumberedList(
		workspaces,
		(membership, index) => {
			const workspace = membership.workspace;
			const itemLines: string[] = [];
			itemLines.push(formatHeading(workspace.name, 2));

			// Basic information
			const properties: Record<string, unknown> = {
				UUID: workspace.uuid,
				Slug: workspace.slug,
				'Permission Level': membership.permission || 'Unknown',
				'Last Accessed': membership.last_accessed
					? formatDate(new Date(membership.last_accessed))
					: 'N/A',
				'Added On': membership.added_on
					? formatDate(new Date(membership.added_on))
					: 'N/A',
				'Web URL': workspace.links?.html?.href
					? formatUrl(workspace.links.html.href, workspace.slug)
					: formatUrl(
							`https://bitbucket.org/${workspace.slug}/`,
							workspace.slug,
						),
				User:
					membership.user?.display_name ||
					membership.user?.nickname ||
					'Unknown',
			};

			// Format as a bullet list
			itemLines.push(formatBulletList(properties, (key) => key));

			// Add separator between workspaces except for the last one
			if (index < workspaces.length - 1) {
				itemLines.push('');
				itemLines.push(formatSeparator());
			}

			return itemLines.join('\n');
		},
	);

	lines.push(formattedList);

	// Add standard footer with timestamp
	lines.push('\n\n' + formatSeparator());
	lines.push(`*Information retrieved at: ${formatDate(new Date())}*`);

	return lines.join('\n');
}

/**
 * Format detailed workspace information for display
 * @param workspace - Raw workspace data from the API
 * @param membership - Optional membership information for the workspace
 * @returns Formatted string with workspace details in markdown format
 */
export function formatWorkspaceDetails(
	workspace: WorkspaceDetailed,
	membership?: WorkspaceMembership,
): string {
	const lines: string[] = [
		formatHeading(`Workspace: ${workspace.name}`, 1),
		'',
		formatHeading('Basic Information', 2),
	];

	// Format basic information as a bullet list
	const basicProperties: Record<string, unknown> = {
		UUID: workspace.uuid,
		Slug: workspace.slug,
		Type: workspace.type || 'Not specified',
		'Created On': workspace.created_on
			? formatDate(workspace.created_on)
			: 'N/A',
	};

	lines.push(formatBulletList(basicProperties, (key) => key));

	// Add membership information if available
	if (membership) {
		lines.push('');
		lines.push(formatHeading('Your Membership', 2));

		const membershipProperties: Record<string, unknown> = {
			Permission: membership.permission,
			'Last Accessed': membership.last_accessed
				? formatDate(membership.last_accessed)
				: 'N/A',
			'Added On': membership.added_on
				? formatDate(membership.added_on)
				: 'N/A',
		};

		lines.push(formatBulletList(membershipProperties, (key) => key));
	}

	// Add links
	lines.push('');
	lines.push(formatHeading('Links', 2));

	const links: string[] = [];

	if (workspace.links.html?.href) {
		links.push(
			`- ${formatUrl(workspace.links.html.href, 'View in Browser')}`,
		);
	}
	if (workspace.links.repositories?.href) {
		links.push(
			`- ${formatUrl(workspace.links.repositories.href, 'Repositories')}`,
		);
	}
	if (workspace.links.projects?.href) {
		links.push(`- ${formatUrl(workspace.links.projects.href, 'Projects')}`);
	}
	if (workspace.links.snippets?.href) {
		links.push(`- ${formatUrl(workspace.links.snippets.href, 'Snippets')}`);
	}

	lines.push(links.join('\n'));

	// Add standard footer with timestamp
	lines.push('\n\n' + formatSeparator());
	lines.push(`*Information retrieved at: ${formatDate(new Date())}*`);

	return lines.join('\n');
}

```

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

```json
{
  "name": "@aashari/mcp-server-atlassian-bitbucket",
  "version": "1.45.0",
  "description": "Node.js/TypeScript MCP server for Atlassian Bitbucket. Enables AI systems (LLMs) to interact with workspaces, repositories, and pull requests via tools (list, get, comment, search). Connects AI directly to version control workflows through the standard MCP interface.",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "type": "commonjs",
  "repository": {
    "type": "git",
    "url": "https://github.com/aashari/mcp-server-atlassian-bitbucket.git"
  },
  "bin": {
    "mcp-atlassian-bitbucket": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "prepare": "npm run build && node scripts/ensure-executable.js",
    "postinstall": "node scripts/ensure-executable.js",
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:cli": "jest src/cli/.*\\.cli\\.test\\.ts --runInBand --testTimeout=60000",
    "lint": "eslint src --ext .ts --config eslint.config.mjs",
    "format": "prettier --write 'src/**/*.ts' 'scripts/**/*.js'",
    "publish:npm": "npm publish",
    "update:check": "npx npm-check-updates",
    "update:deps": "npx npm-check-updates -u && npm install --legacy-peer-deps",
    "update:version": "node scripts/update-version.js",
    "mcp:stdio": "TRANSPORT_MODE=stdio npm run build && node dist/index.js",
    "mcp:http": "TRANSPORT_MODE=http npm run build && node dist/index.js",
    "mcp:inspect": "TRANSPORT_MODE=http npm run build && (node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp",
    "dev:stdio": "npm run build && npx @modelcontextprotocol/inspector -e TRANSPORT_MODE=stdio -e DEBUG=true node dist/index.js",
    "dev:http": "DEBUG=true TRANSPORT_MODE=http npm run build && node dist/index.js",
    "dev:server": "DEBUG=true npm run build && npx @modelcontextprotocol/inspector -e DEBUG=true node dist/index.js",
    "dev:cli": "DEBUG=true npm run build && DEBUG=true node dist/index.js",
    "start:server": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
    "start:cli": "npm run build && node dist/index.js"
  },
  "keywords": [
    "mcp",
    "typescript",
    "claude",
    "anthropic",
    "ai",
    "atlassian",
    "bitbucket",
    "repository",
    "version-control",
    "pull-request",
    "server",
    "model-context-protocol",
    "tools",
    "resources",
    "tooling",
    "ai-integration",
    "mcp-server",
    "llm",
    "ai-connector",
    "external-tools",
    "cli",
    "mcp-inspector"
  ],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@eslint/js": "^9.35.0",
    "@semantic-release/changelog": "^6.0.3",
    "@semantic-release/exec": "^7.1.0",
    "@semantic-release/git": "^10.0.1",
    "@semantic-release/github": "^11.0.5",
    "@semantic-release/npm": "^12.0.2",
    "@types/cors": "^2.8.19",
    "@types/express": "^5.0.3",
    "@types/jest": "^30.0.0",
    "@types/node": "^24.3.1",
    "@types/turndown": "^5.0.5",
    "@typescript-eslint/eslint-plugin": "^8.43.0",
    "@typescript-eslint/parser": "^8.43.0",
    "eslint": "^9.35.0",
    "eslint-config-prettier": "^10.1.8",
    "eslint-plugin-filenames": "^1.3.2",
    "eslint-plugin-prettier": "^5.5.4",
    "jest": "^30.1.3",
    "nodemon": "^3.1.10",
    "npm-check-updates": "^18.1.0",
    "prettier": "^3.6.2",
    "semantic-release": "^24.2.7",
    "ts-jest": "^29.4.1",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.2",
    "typescript-eslint": "^8.43.0"
  },
  "publishConfig": {
    "registry": "https://registry.npmjs.org/",
    "access": "public"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.17.5",
    "commander": "^14.0.0",
    "cors": "^2.8.5",
    "dotenv": "^17.2.2",
    "express": "^5.1.0",
    "turndown": "^7.2.1",
    "zod": "^3.25.76"
  },
  "directories": {
    "example": "examples"
  },
  "jest": {
    "preset": "ts-jest",
    "testEnvironment": "node",
    "setupFilesAfterEnv": [
      "<rootDir>/jest.setup.js"
    ],
    "testMatch": [
      "**/src/**/*.test.ts"
    ],
    "collectCoverageFrom": [
      "src/**/*.ts",
      "!src/**/*.test.ts",
      "!src/**/*.spec.ts"
    ],
    "coveragePathIgnorePatterns": [
      "/node_modules/",
      "/dist/",
      "/coverage/"
    ],
    "coverageReporters": [
      "text",
      "lcov",
      "json-summary"
    ],
    "transform": {
      "^.+\\.tsx?$": [
        "ts-jest",
        {
          "useESM": true
        }
      ]
    },
    "moduleNameMapper": {
      "(.*)\\.(js|jsx)$": "$1"
    },
    "extensionsToTreatAsEsm": [
      ".ts"
    ],
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx",
      "json",
      "node"
    ]
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

```

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

```typescript
import { ControllerResponse } from '../types/common.types.js';
import {
	ListPullRequestsToolArgsType,
	GetPullRequestToolArgsType,
	ListPullRequestCommentsToolArgsType,
	CreatePullRequestCommentToolArgsType,
	CreatePullRequestToolArgsType,
	UpdatePullRequestToolArgsType,
	ApprovePullRequestToolArgsType,
	RejectPullRequestToolArgsType,
} from '../tools/atlassian.pullrequests.types.js';

import listController from './atlassian.pullrequests.list.controller.js';
import getController from './atlassian.pullrequests.get.controller.js';
import commentsController from './atlassian.pullrequests.comments.controller.js';
import createController from './atlassian.pullrequests.create.controller.js';
import updateController from './atlassian.pullrequests.update.controller.js';
import approveController from './atlassian.pullrequests.approve.controller.js';
import rejectController from './atlassian.pullrequests.reject.controller.js';

/**
 * Controller for managing Bitbucket pull requests.
 * Provides functionality for listing, retrieving, and creating pull requests and comments.
 *
 * NOTE ON MARKDOWN HANDLING:
 * Unlike Jira (which uses ADF) or Confluence (which uses a mix of formats),
 * Bitbucket Cloud API natively accepts Markdown for text content in both directions:
 * - When sending data TO the API (comments, PR descriptions)
 * - When receiving data FROM the API (PR descriptions, comments)
 *
 * The API expects content in the format: { content: { raw: "markdown-text" } }
 *
 * We use optimizeBitbucketMarkdown() to address specific rendering quirks in
 * Bitbucket's markdown renderer but it does NOT perform format conversion.
 * See formatter.util.ts for details on the specific issues it addresses.
 */

/**
 * List Bitbucket pull requests with optional filtering options
 * @param options - Options for listing pull requests including workspace slug and repo slug
 * @returns Promise with formatted pull requests list content and pagination information
 */
async function list(
	options: ListPullRequestsToolArgsType,
): Promise<ControllerResponse> {
	return listController.list(options);
}

/**
 * Get detailed information about a specific Bitbucket pull request
 * @param options - Options including workspace slug, repo slug, and pull request ID
 * @returns Promise with formatted pull request details as Markdown content
 */
async function get(
	options: GetPullRequestToolArgsType,
): Promise<ControllerResponse> {
	return getController.get(options);
}

/**
 * List comments on a Bitbucket pull request
 * @param options - Options including workspace slug, repo slug, and pull request ID
 * @returns Promise with formatted pull request comments as Markdown content
 */
async function listComments(
	options: ListPullRequestCommentsToolArgsType,
): Promise<ControllerResponse> {
	return commentsController.listComments(options);
}

/**
 * Add a comment to a Bitbucket pull request
 * @param options - Options including workspace slug, repo slug, PR ID, and comment content
 * @returns Promise with a success message as content
 */
async function addComment(
	options: CreatePullRequestCommentToolArgsType,
): Promise<ControllerResponse> {
	return commentsController.addComment(options);
}

/**
 * Create a new pull request in Bitbucket
 * @param options - Options including workspace slug, repo slug, source branch, target branch, title, etc.
 * @returns Promise with formatted pull request details as Markdown content
 */
async function add(
	options: CreatePullRequestToolArgsType,
): Promise<ControllerResponse> {
	return createController.add(options);
}

/**
 * Update an existing pull request in Bitbucket
 * @param options - Options including workspace slug, repo slug, pull request ID, title, and description
 * @returns Promise with formatted updated pull request details as Markdown content
 */
async function update(
	options: UpdatePullRequestToolArgsType,
): Promise<ControllerResponse> {
	return updateController.update(options);
}

/**
 * Approve a pull request in Bitbucket
 * @param options - Options including workspace slug, repo slug, and pull request ID
 * @returns Promise with formatted approval confirmation as Markdown content
 */
async function approve(
	options: ApprovePullRequestToolArgsType,
): Promise<ControllerResponse> {
	return approveController.approve(options);
}

/**
 * Request changes on a pull request in Bitbucket
 * @param options - Options including workspace slug, repo slug, and pull request ID
 * @returns Promise with formatted rejection confirmation as Markdown content
 */
async function reject(
	options: RejectPullRequestToolArgsType,
): Promise<ControllerResponse> {
	return rejectController.reject(options);
}

// Export the controller functions
export default {
	list,
	get,
	listComments,
	addComment,
	add,
	update,
	approve,
	reject,
};

```

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

```typescript
import atlassianWorkspacesService from '../services/vendor.atlassian.workspaces.service.js';
import { Logger } from '../utils/logger.util.js';
import { handleControllerError } from '../utils/error-handler.util.js';
import {
	extractPaginationInfo,
	PaginationType,
} from '../utils/pagination.util.js';
import { ControllerResponse } from '../types/common.types.js';
import {
	ListWorkspacesToolArgsType,
	GetWorkspaceToolArgsType,
} from '../tools/atlassian.workspaces.types.js';
import {
	formatWorkspacesList,
	formatWorkspaceDetails,
} from './atlassian.workspaces.formatter.js';
import { ListWorkspacesParams } from '../services/vendor.atlassian.workspaces.types.js';
import { DEFAULT_PAGE_SIZE, applyDefaults } from '../utils/defaults.util.js';
import { formatPagination } from '../utils/formatter.util.js';

// Create a contextualized logger for this file
const controllerLogger = Logger.forContext(
	'controllers/atlassian.workspaces.controller.ts',
);

// Log controller initialization
controllerLogger.debug('Bitbucket workspaces controller initialized');

/**
 * Controller for managing Bitbucket workspaces.
 * Provides functionality for listing workspaces and retrieving workspace details.
 */

/**
 * List Bitbucket workspaces with optional filtering
 * @param options - Options for listing workspaces
 * @param options.limit - Maximum number of workspaces to return
 * @param options.cursor - Pagination cursor for retrieving the next set of results
 * @returns Promise with formatted workspace list content including pagination information
 */
async function list(
	options: ListWorkspacesToolArgsType,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.workspaces.controller.ts',
		'list',
	);
	methodLogger.debug('Listing Bitbucket workspaces...', options);

	try {
		// Create defaults object with proper typing
		const defaults: Partial<ListWorkspacesToolArgsType> = {
			limit: DEFAULT_PAGE_SIZE,
		};

		// Apply defaults
		const mergedOptions = applyDefaults<ListWorkspacesToolArgsType>(
			options,
			defaults,
		);

		// Map controller filters to service params
		const serviceParams: ListWorkspacesParams = {
			pagelen: mergedOptions.limit, // Default page length
			page: mergedOptions.cursor
				? parseInt(mergedOptions.cursor, 10)
				: undefined, // Use cursor value for page
			// NOTE: Sort parameter is not included as the Bitbucket API's /2.0/user/permissions/workspaces
			// endpoint does not support sorting on any field
		};

		methodLogger.debug('Using filters:', serviceParams);

		const workspacesData =
			await atlassianWorkspacesService.list(serviceParams);

		methodLogger.debug(
			`Retrieved ${workspacesData.values?.length || 0} workspaces`,
		);

		// Extract pagination information using the utility
		const pagination = extractPaginationInfo(
			workspacesData,
			PaginationType.PAGE,
		);

		// Format the workspaces data for display using the formatter
		const formattedWorkspaces = formatWorkspacesList(workspacesData);

		// Create the final content by combining the formatted workspaces with pagination information
		let finalContent = formattedWorkspaces;

		// Add pagination information if available
		if (
			pagination &&
			(pagination.hasMore || pagination.count !== undefined)
		) {
			const paginationString = formatPagination(pagination);
			finalContent += '\n\n' + paginationString;
		}

		return {
			content: finalContent,
		};
	} catch (error) {
		// Use the standardized error handler
		throw handleControllerError(error, {
			entityType: 'Workspaces',
			operation: 'listing',
			source: 'controllers/atlassian.workspaces.controller.ts@list',
			additionalInfo: { options },
		});
	}
}

/**
 * Get details of a specific Bitbucket workspace
 * @param identifier - Object containing the workspace slug
 * @param identifier.workspaceSlug - The slug of the workspace to retrieve
 * @returns Promise with formatted workspace details content
 * @throws Error if workspace retrieval fails
 */
async function get(
	identifier: GetWorkspaceToolArgsType,
): Promise<ControllerResponse> {
	const { workspaceSlug } = identifier;
	const methodLogger = Logger.forContext(
		'controllers/atlassian.workspaces.controller.ts',
		'get',
	);

	methodLogger.debug(
		`Getting Bitbucket workspace with slug: ${workspaceSlug}...`,
	);

	try {
		const workspaceData =
			await atlassianWorkspacesService.get(workspaceSlug);
		methodLogger.debug(`Retrieved workspace: ${workspaceData.slug}`);

		// Since membership info isn't directly available, we'll use the workspace data only
		methodLogger.debug(
			'Membership info not available, using workspace data only',
		);

		// Format the workspace data for display using the formatter
		const formattedWorkspace = formatWorkspaceDetails(
			workspaceData,
			undefined, // Pass undefined instead of membership data
		);

		return {
			content: formattedWorkspace,
		};
	} catch (error) {
		// Use the standardized error handler
		throw handleControllerError(error, {
			entityType: 'Workspace',
			operation: 'retrieving',
			source: 'controllers/atlassian.workspaces.controller.ts@get',
			additionalInfo: { identifier },
		});
	}
}

export default { list, get };

```

--------------------------------------------------------------------------------
/src/controllers/atlassian.workspaces.controller.test.ts:
--------------------------------------------------------------------------------

```typescript
import atlassianWorkspacesController from './atlassian.workspaces.controller.js';
import { getAtlassianCredentials } from '../utils/transport.util.js';
import { config } from '../utils/config.util.js';
import { McpError } from '../utils/error.util.js';

describe('Atlassian Workspaces Controller', () => {
	// Load configuration and check for credentials before all tests
	beforeAll(() => {
		config.load(); // Ensure config is loaded
		const credentials = getAtlassianCredentials();
		if (!credentials) {
			console.warn(
				'Skipping Atlassian Workspaces Controller tests: No credentials available',
			);
		}
	});

	// Helper function to skip tests when credentials are missing
	const skipIfNoCredentials = () => !getAtlassianCredentials();

	describe('list', () => {
		it('should return a formatted list of workspaces in Markdown', async () => {
			if (skipIfNoCredentials()) return;

			const result = await atlassianWorkspacesController.list({});

			// Verify the response structure
			expect(result).toHaveProperty('content');
			expect(typeof result.content).toBe('string');

			// Basic Markdown content checks
			if (result.content !== 'No Bitbucket workspaces found.') {
				expect(result.content).toMatch(/^# Bitbucket Workspaces/m);
				expect(result.content).toContain('**UUID**');
				expect(result.content).toContain('**Slug**');
				expect(result.content).toContain('**Permission Level**');

				// Check for pagination information in the content string
				expect(result.content).toMatch(
					/---\s*[\s\S]*\*Showing \d+ (of \d+ total items|\S+ items?)[\s\S]*\*/,
				);
			}
		}, 30000); // Increased timeout

		it('should handle pagination options (limit/cursor)', async () => {
			if (skipIfNoCredentials()) return;

			// Fetch first page
			const result1 = await atlassianWorkspacesController.list({
				limit: 1,
			});

			// Extract pagination info from content
			const countMatch1 = result1.content.match(
				/\*Showing (\d+) items?\.\*/,
			);
			const count1 = countMatch1 ? parseInt(countMatch1[1], 10) : 0;
			expect(count1).toBeLessThanOrEqual(1);

			// Extract cursor from content
			const cursorMatch1 = result1.content.match(
				/\*Next cursor: `(\d+)`\*/,
			);
			const nextCursor = cursorMatch1 ? cursorMatch1[1] : null;

			// Check if pagination indicates more results
			const hasMoreResults = result1.content.includes(
				'More results are available.',
			);

			// If there's a next page, fetch it
			if (hasMoreResults && nextCursor) {
				const result2 = await atlassianWorkspacesController.list({
					limit: 1,
					cursor: nextCursor,
				});

				// Ensure content is different (or handle case where only 1 item exists)
				if (
					result1.content !== 'No Bitbucket workspaces found.' &&
					result2.content !== 'No Bitbucket workspaces found.'
				) {
					// Only compare if we actually have multiple workspaces
					expect(result1.content).not.toEqual(result2.content);
				}
			} else {
				console.warn(
					'Skipping cursor part of pagination test: Only one page of workspaces found.',
				);
			}
		}, 30000);
	});

	describe('get', () => {
		// Helper to get a valid slug for testing 'get'
		async function getFirstWorkspaceSlugForController(): Promise<
			string | null
		> {
			if (skipIfNoCredentials()) return null;
			try {
				const listResult = await atlassianWorkspacesController.list({
					limit: 1,
				});
				if (listResult.content === 'No Bitbucket workspaces found.')
					return null;
				// Extract slug from Markdown content
				const slugMatch = listResult.content.match(
					/\*\*Slug\*\*:\s+([^\s\n]+)/,
				);
				return slugMatch ? slugMatch[1] : null;
			} catch (error) {
				console.warn(
					"Could not fetch workspace list for controller 'get' test setup:",
					error,
				);
				return null;
			}
		}

		it('should return formatted details for a valid workspace slug in Markdown', async () => {
			const workspaceSlug = await getFirstWorkspaceSlugForController();
			if (!workspaceSlug) {
				console.warn(
					'Skipping controller get test: No workspace slug found.',
				);
				return;
			}

			const result = await atlassianWorkspacesController.get({
				workspaceSlug,
			});

			// Verify the ControllerResponse structure
			expect(result).toHaveProperty('content');
			expect(typeof result.content).toBe('string');

			// Verify Markdown content
			expect(result.content).toMatch(/^# Workspace:/m);
			expect(result.content).toContain(`**Slug**: ${workspaceSlug}`);
			expect(result.content).toContain('## Basic Information');
			expect(result.content).toContain('## Links');
		}, 30000);

		it('should throw McpError for an invalid workspace slug', async () => {
			if (skipIfNoCredentials()) return;

			const invalidSlug = 'this-slug-definitely-does-not-exist-12345';

			// Expect the controller call to reject with an McpError
			await expect(
				atlassianWorkspacesController.get({
					workspaceSlug: invalidSlug,
				}),
			).rejects.toThrow(McpError);

			// Optionally check the status code via the error handler's behavior
			try {
				await atlassianWorkspacesController.get({
					workspaceSlug: invalidSlug,
				});
			} catch (e) {
				expect(e).toBeInstanceOf(McpError);
				// The controller error handler wraps the service error
				expect((e as McpError).statusCode).toBe(404); // Expecting Not Found
				expect((e as McpError).message).toContain('not found');
			}
		}, 30000);
	});
});

```

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

```typescript
import { CliTestUtil } from '../utils/cli.test.util.js';
import { getAtlassianCredentials } from '../utils/transport.util.js';
import { config } from '../utils/config.util.js';

describe('Atlassian Workspaces CLI Commands', () => {
	// Load configuration and check for credentials before all tests
	beforeAll(() => {
		// Load configuration from all sources
		config.load();

		// Log warning if credentials aren't available
		const credentials = getAtlassianCredentials();
		if (!credentials) {
			console.warn(
				'Skipping Atlassian Workspaces CLI tests: No credentials available',
			);
		}
	});

	// Helper function to skip tests when credentials are missing
	const skipIfNoCredentials = () => {
		const credentials = getAtlassianCredentials();
		if (!credentials) {
			return true;
		}
		return false;
	};

	describe('ls-workspaces command', () => {
		// Test default behavior (list all workspaces)
		it('should list available workspaces', async () => {
			if (skipIfNoCredentials()) {
				return;
			}

			// Run the CLI command
			const result = await CliTestUtil.runCommand(['ls-workspaces']);

			// Check command exit code
			expect(result.exitCode).toBe(0);

			// Verify the output format
			if (!result.stdout.includes('No Bitbucket workspaces found.')) {
				// Validate expected Markdown structure - Fixed to match actual output
				CliTestUtil.validateOutputContains(result.stdout, [
					'# Bitbucket Workspaces',
					'**UUID**',
					'**Slug**',
					'**Permission Level**',
				]);

				// Validate Markdown formatting
				CliTestUtil.validateMarkdownOutput(result.stdout);
			}
		}, 30000); // Increased timeout for API call

		// Test with pagination
		it('should support pagination with --limit flag', async () => {
			if (skipIfNoCredentials()) {
				return;
			}

			// Run the CLI command with limit
			const result = await CliTestUtil.runCommand([
				'ls-workspaces',
				'--limit',
				'1',
			]);

			// Check command exit code
			expect(result.exitCode).toBe(0);

			// If there are multiple workspaces, pagination section should be present
			if (
				!result.stdout.includes('No Bitbucket workspaces found.') &&
				result.stdout.includes('items remaining')
			) {
				CliTestUtil.validateOutputContains(result.stdout, [
					'Pagination',
					'Next cursor:',
				]);
			}
		}, 30000); // Increased timeout for API call

		// Test with invalid parameters - Fixed to use a truly invalid input
		it('should handle invalid parameters properly', async () => {
			if (skipIfNoCredentials()) {
				return;
			}

			// Run the CLI command with a non-existent parameter
			const result = await CliTestUtil.runCommand([
				'ls-workspaces',
				'--non-existent-parameter',
				'value',
			]);

			// Should fail with non-zero exit code
			expect(result.exitCode).not.toBe(0);

			// Should output error message
			expect(result.stderr).toContain('unknown option');
		}, 30000);
	});

	describe('get-workspace command', () => {
		// Test to fetch a specific workspace
		it('should retrieve a specific workspace by slug', async () => {
			if (skipIfNoCredentials()) {
				return;
			}

			// First, get a list of workspaces to find a valid slug
			const listResult = await CliTestUtil.runCommand(['ls-workspaces']);

			// Skip if no workspaces are available
			if (listResult.stdout.includes('No Bitbucket workspaces found.')) {
				console.warn('Skipping test: No workspaces available');
				return;
			}

			// Extract a workspace slug from the output
			const slugMatch = listResult.stdout.match(
				/\*\*Slug\*\*:\s+([^\n]+)/,
			);
			if (!slugMatch || !slugMatch[1]) {
				console.warn('Skipping test: Could not extract workspace slug');
				return;
			}

			const workspaceSlug = slugMatch[1].trim();

			// Run the get-workspace command with the extracted slug
			const getResult = await CliTestUtil.runCommand([
				'get-workspace',
				'--workspace-slug',
				workspaceSlug,
			]);

			// Check command exit code
			expect(getResult.exitCode).toBe(0);

			// Verify the output structure and content
			CliTestUtil.validateOutputContains(getResult.stdout, [
				`# Workspace: `,
				`**Slug**: ${workspaceSlug}`,
				'Basic Information',
				'Links',
			]);

			// Validate Markdown formatting
			CliTestUtil.validateMarkdownOutput(getResult.stdout);
		}, 30000); // Increased timeout for API calls

		// Test with missing required parameter
		it('should fail when workspace slug is not provided', async () => {
			if (skipIfNoCredentials()) {
				return;
			}

			// Run command without required parameter
			const result = await CliTestUtil.runCommand(['get-workspace']);

			// Should fail with non-zero exit code
			expect(result.exitCode).not.toBe(0);

			// Should indicate missing required option
			expect(result.stderr).toContain('required option');
		}, 15000);

		// Test with invalid workspace slug
		it('should handle invalid workspace slugs gracefully', async () => {
			if (skipIfNoCredentials()) {
				return;
			}

			// Use a deliberately invalid workspace slug
			const invalidSlug = 'invalid-workspace-slug-that-does-not-exist';

			// Run command with invalid slug
			const result = await CliTestUtil.runCommand([
				'get-workspace',
				'--workspace-slug',
				invalidSlug,
			]);

			// Should fail with non-zero exit code
			expect(result.exitCode).not.toBe(0);

			// Should contain error information
			expect(result.stderr).toContain('error');
		}, 30000);
	});
});

```

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

```typescript
import { Logger } from '../utils/logger.util.js';
import { ControllerResponse } from '../types/common.types.js';
import { DEFAULT_PAGE_SIZE } from '../utils/defaults.util.js';
import atlassianSearchService from '../services/vendor.atlassian.search.service.js';
import {
	extractPaginationInfo,
	PaginationType,
} from '../utils/pagination.util.js';
import { formatPagination } from '../utils/formatter.util.js';
import { formatCodeSearchResults } from './atlassian.search.formatter.js';

/**
 * Handle search for code content (uses Bitbucket's Code Search API)
 */
export async function handleCodeSearch(
	workspaceSlug: string,
	repoSlug?: string,
	query?: string,
	limit: number = DEFAULT_PAGE_SIZE,
	cursor?: string,
	language?: string,
	extension?: string,
): Promise<ControllerResponse> {
	const methodLogger = Logger.forContext(
		'controllers/atlassian.search.code.controller.ts',
		'handleCodeSearch',
	);
	methodLogger.debug('Performing code search');

	if (!query) {
		return {
			content: 'Please provide a search query for code search.',
		};
	}

	try {
		// Convert cursor to page number if provided
		let page = 1;
		if (cursor) {
			const parsedPage = parseInt(cursor, 10);
			if (!isNaN(parsedPage)) {
				page = parsedPage;
			} else {
				methodLogger.warn('Invalid page cursor:', cursor);
			}
		}

		// Use the search service
		const searchResponse = await atlassianSearchService.searchCode({
			workspaceSlug: workspaceSlug,
			searchQuery: query,
			repoSlug: repoSlug,
			page: page,
			pageLen: limit,
			language: language,
			extension: extension,
		});

		methodLogger.debug(
			`Search complete, found ${searchResponse.size} matches`,
		);

		// Post-filter by language if specified and Bitbucket API returned mixed results
		let filteredValues = searchResponse.values || [];
		let originalSize = searchResponse.size;

		if (language && filteredValues.length > 0) {
			// Language extension mapping for post-filtering
			const languageExtMap: Record<string, string[]> = {
				hcl: ['.tf', '.tfvars', '.hcl'],
				terraform: ['.tf', '.tfvars', '.hcl'],
				java: ['.java', '.class', '.jar'],
				javascript: ['.js', '.jsx', '.mjs'],
				typescript: ['.ts', '.tsx'],
				python: ['.py', '.pyw', '.pyc'],
				ruby: ['.rb', '.rake'],
				go: ['.go'],
				rust: ['.rs'],
				c: ['.c', '.h'],
				cpp: ['.cpp', '.cc', '.cxx', '.h', '.hpp'],
				csharp: ['.cs'],
				php: ['.php'],
				html: ['.html', '.htm'],
				css: ['.css'],
				shell: ['.sh', '.bash', '.zsh'],
				sql: ['.sql'],
				yaml: ['.yml', '.yaml'],
				json: ['.json'],
				xml: ['.xml'],
				markdown: ['.md', '.markdown'],
			};

			// Normalize the language name to lowercase
			const normalizedLang = language.toLowerCase();
			const extensions = languageExtMap[normalizedLang] || [];

			// Only apply post-filtering if we have extension mappings for this language
			if (extensions.length > 0) {
				const beforeFilterCount = filteredValues.length;

				// Filter results to only include files with the expected extensions
				filteredValues = filteredValues.filter((result) => {
					const filePath = result.file.path.toLowerCase();
					return extensions.some((ext) => filePath.endsWith(ext));
				});

				const afterFilterCount = filteredValues.length;

				if (afterFilterCount !== beforeFilterCount) {
					methodLogger.debug(
						`Post-filtered code search results by language=${language}: ${afterFilterCount} of ${beforeFilterCount} matched extensions ${extensions.join(', ')}`,
					);

					// Adjust the size estimate
					originalSize = searchResponse.size;
					const filterRatio = afterFilterCount / beforeFilterCount;
					searchResponse.size = Math.max(
						afterFilterCount,
						Math.ceil(searchResponse.size * filterRatio),
					);

					methodLogger.debug(
						`Adjusted size from ${originalSize} to ${searchResponse.size} based on filtering`,
					);
				}
			}
		}

		// Extract pagination information
		const transformedResponse = {
			pagelen: limit,
			page: page,
			size: searchResponse.size,
			values: filteredValues,
			next: 'available', // Fallback to 'available' since searchResponse doesn't have a next property
		};

		const pagination = extractPaginationInfo(
			transformedResponse,
			PaginationType.PAGE,
		);

		// Format the code search results
		let formattedCode = formatCodeSearchResults({
			...searchResponse,
			values: filteredValues,
		});

		// Add note about language filtering if applied
		if (language) {
			// Make it clear that language filtering is a best-effort by the API and we've improved it
			const languageNote = `> **Note:** Language filtering for '${language}' combines Bitbucket API filtering with client-side filtering for more accurate results. Due to limitations in the Bitbucket API, some files in other languages might still appear in search results, and filtering is based on file extensions rather than content analysis. This is a known limitation of the Bitbucket API that this tool attempts to mitigate through additional filtering.`;
			formattedCode = `${languageNote}\n\n${formattedCode}`;
		}

		// Add pagination information if available
		let finalContent = formattedCode;
		if (
			pagination &&
			(pagination.hasMore || pagination.count !== undefined)
		) {
			const paginationString = formatPagination(pagination);
			finalContent += '\n\n' + paginationString;
		}

		return {
			content: finalContent,
		};
	} catch (searchError) {
		methodLogger.error('Error performing code search:', searchError);
		throw searchError;
	}
}

```

--------------------------------------------------------------------------------
/scripts/update-version.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

/**
 * Script to update version numbers across the project
 * Usage: node scripts/update-version.js [version] [options]
 * Options:
 *   --dry-run   Show what changes would be made without applying them
 *   --verbose   Show detailed logging information
 *
 * If no version is provided, it will use the version from package.json
 */

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// Get the directory name of the current module
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..');

// Parse command line arguments
const args = process.argv.slice(2);
const options = {
	dryRun: args.includes('--dry-run'),
	verbose: args.includes('--verbose'),
};

// Get the version (first non-flag argument)
let newVersion = args.find((arg) => !arg.startsWith('--'));

// Log helper function
const log = (message, verbose = false) => {
	if (!verbose || options.verbose) {
		console.log(message);
	}
};

// File paths that may contain version information
const versionFiles = [
	{
		path: path.join(rootDir, 'package.json'),
		pattern: /"version": "([^"]*)"/,
		replacement: (match, currentVersion) =>
			match.replace(currentVersion, newVersion),
	},
	{
		path: path.join(rootDir, 'src', 'utils', 'constants.util.ts'),
		pattern: /export const VERSION = ['"]([^'"]*)['"]/,
		replacement: (match, currentVersion) =>
			match.replace(currentVersion, newVersion),
	},
	// Also update the compiled JavaScript files if they exist
	{
		path: path.join(rootDir, 'dist', 'utils', 'constants.util.js'),
		pattern: /exports.VERSION = ['"]([^'"]*)['"]/,
		replacement: (match, currentVersion) =>
			match.replace(currentVersion, newVersion),
		optional: true, // Mark this file as optional
	},
	// Additional files can be added here with their patterns and replacement logic
];

/**
 * Read the version from package.json
 * @returns {string} The version from package.json
 */
function getPackageVersion() {
	try {
		const packageJsonPath = path.join(rootDir, 'package.json');
		log(`Reading version from ${packageJsonPath}`, true);

		const packageJson = JSON.parse(
			fs.readFileSync(packageJsonPath, 'utf8'),
		);

		if (!packageJson.version) {
			throw new Error('No version field found in package.json');
		}

		return packageJson.version;
	} catch (error) {
		console.error(`Error reading package.json: ${error.message}`);
		process.exit(1);
	}
}

/**
 * Validate the semantic version format
 * @param {string} version - The version to validate
 * @returns {boolean} True if valid, throws error if invalid
 */
function validateVersion(version) {
	// More comprehensive semver regex
	const semverRegex =
		/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;

	if (!semverRegex.test(version)) {
		throw new Error(
			`Invalid version format: ${version}\nPlease use semantic versioning format (e.g., 1.2.3, 1.2.3-beta.1, etc.)`,
		);
	}

	return true;
}

/**
 * Update version in a specific file
 * @param {Object} fileConfig - Configuration for the file to update
 */
function updateFileVersion(fileConfig) {
	const {
		path: filePath,
		pattern,
		replacement,
		optional = false,
	} = fileConfig;

	try {
		log(`Checking ${filePath}...`, true);

		if (!fs.existsSync(filePath)) {
			if (optional) {
				log(`Optional file not found (skipping): ${filePath}`, true);
				return;
			}
			console.warn(`Warning: File not found: ${filePath}`);
			return;
		}

		// Read file content
		const fileContent = fs.readFileSync(filePath, 'utf8');
		const match = fileContent.match(pattern);

		if (!match) {
			console.warn(`Warning: Version pattern not found in ${filePath}`);
			return;
		}

		const currentVersion = match[1];
		if (currentVersion === newVersion) {
			log(
				`Version in ${path.basename(filePath)} is already ${newVersion}`,
				true,
			);
			return;
		}

		// Create new content with the updated version
		const updatedContent = fileContent.replace(pattern, replacement);

		// Write the changes or log them in dry run mode
		if (options.dryRun) {
			log(
				`Would update version in ${filePath} from ${currentVersion} to ${newVersion}`,
			);
		} else {
			// Create a backup of the original file
			fs.writeFileSync(`${filePath}.bak`, fileContent);
			log(`Backup created: ${filePath}.bak`, true);

			// Write the updated content
			fs.writeFileSync(filePath, updatedContent);
			log(
				`Updated version in ${path.basename(filePath)} from ${currentVersion} to ${newVersion}`,
			);
		}
	} catch (error) {
		if (optional) {
			log(`Error with optional file ${filePath}: ${error.message}`, true);
			return;
		}
		console.error(`Error updating ${filePath}: ${error.message}`);
		process.exit(1);
	}
}

// Main execution
try {
	// If no version specified, get from package.json
	if (!newVersion) {
		newVersion = getPackageVersion();
		log(
			`No version specified, using version from package.json: ${newVersion}`,
		);
	}

	// Validate the version format
	validateVersion(newVersion);

	// Update all configured files
	for (const fileConfig of versionFiles) {
		updateFileVersion(fileConfig);
	}

	if (options.dryRun) {
		log(`\nDry run completed. No files were modified.`);
	} else {
		log(`\nVersion successfully updated to ${newVersion}`);
	}
} catch (error) {
	console.error(`\nVersion update failed: ${error.message}`);
	process.exit(1);
}

```

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

```typescript
import atlassianWorkspacesService from './vendor.atlassian.workspaces.service.js';
import { getAtlassianCredentials } from '../utils/transport.util.js';
import { config } from '../utils/config.util.js';
import { McpError } from '../utils/error.util.js';

describe('Vendor Atlassian Workspaces Service', () => {
	// Load configuration and check for credentials before all tests
	beforeAll(() => {
		config.load(); // Ensure config is loaded
		const credentials = getAtlassianCredentials();
		if (!credentials) {
			console.warn(
				'Skipping Atlassian Workspaces Service tests: No credentials available',
			);
		}
	});

	// Helper function to skip tests when credentials are missing
	const skipIfNoCredentials = () => !getAtlassianCredentials();

	describe('list', () => {
		it('should return a list of workspaces (permissions)', async () => {
			if (skipIfNoCredentials()) return;

			const result = await atlassianWorkspacesService.list();

			// Verify the response structure based on WorkspacePermissionsResponse
			expect(result).toHaveProperty('values');
			expect(Array.isArray(result.values)).toBe(true);
			expect(result).toHaveProperty('pagelen'); // Bitbucket uses pagelen
			expect(result).toHaveProperty('page');
			expect(result).toHaveProperty('size');

			if (result.values.length > 0) {
				const membership = result.values[0];
				expect(membership).toHaveProperty(
					'type',
					'workspace_membership',
				);
				expect(membership).toHaveProperty('permission');
				expect(membership).toHaveProperty('user');
				expect(membership).toHaveProperty('workspace');
				expect(membership.workspace).toHaveProperty('slug');
				expect(membership.workspace).toHaveProperty('uuid');
			}
		}, 30000); // Increased timeout

		it('should support pagination with pagelen', async () => {
			if (skipIfNoCredentials()) return;

			const result = await atlassianWorkspacesService.list({
				pagelen: 1,
			});

			expect(result).toHaveProperty('pagelen');
			// Allow pagelen to be greater than requested if API enforces minimum
			expect(result.pagelen).toBeGreaterThanOrEqual(1);
			expect(result.values.length).toBeLessThanOrEqual(result.pagelen); // Items should not exceed pagelen

			if (result.size > result.pagelen) {
				// If there are more items than the page size, expect pagination links
				expect(result).toHaveProperty('next');
			}
		}, 30000);

		it('should handle query filtering if supported by the API', async () => {
			if (skipIfNoCredentials()) return;

			// First get all workspaces to find a potential query term
			const allWorkspaces = await atlassianWorkspacesService.list();

			// Skip if no workspaces available
			if (allWorkspaces.values.length === 0) {
				console.warn(
					'Skipping query filtering test: No workspaces available',
				);
				return;
			}

			// Try to search using a workspace name - note that this might not work if
			// the API doesn't fully support 'q' parameter for this endpoint
			// This test basically checks that the request doesn't fail
			const firstWorkspace = allWorkspaces.values[0].workspace;
			try {
				const result = await atlassianWorkspacesService.list({
					q: `workspace.name="${firstWorkspace.name}"`,
				});

				// We're mostly testing that this request completes without error
				expect(result).toHaveProperty('values');

				// The result might be empty if filtering isn't supported,
				// so we don't assert on the number of results returned
			} catch (error) {
				// If filtering isn't supported, the API might return an error
				// This is acceptable, so we just log it
				console.warn(
					'Query filtering test encountered an error:',
					error instanceof Error ? error.message : String(error),
				);
			}
		}, 30000);
	});

	describe('get', () => {
		// Helper to get a valid slug for testing 'get'
		async function getFirstWorkspaceSlug(): Promise<string | null> {
			if (skipIfNoCredentials()) return null;
			try {
				const listResult = await atlassianWorkspacesService.list({
					pagelen: 1,
				});
				return listResult.values.length > 0
					? listResult.values[0].workspace.slug
					: null;
			} catch (error) {
				console.warn(
					"Could not fetch workspace list for 'get' test setup:",
					error,
				);
				return null;
			}
		}

		it('should return details for a valid workspace slug', async () => {
			const workspaceSlug = await getFirstWorkspaceSlug();
			if (!workspaceSlug) {
				console.warn('Skipping get test: No workspace slug found.');
				return;
			}

			const result = await atlassianWorkspacesService.get(workspaceSlug);

			// Verify the response structure based on WorkspaceDetailed
			expect(result).toHaveProperty('uuid');
			expect(result).toHaveProperty('slug', workspaceSlug);
			expect(result).toHaveProperty('name');
			expect(result).toHaveProperty('type', 'workspace');
			expect(result).toHaveProperty('links');
			expect(result.links).toHaveProperty('html');
		}, 30000);

		it('should throw an McpError for an invalid workspace slug', async () => {
			if (skipIfNoCredentials()) return;

			const invalidSlug = 'this-slug-definitely-does-not-exist-12345';

			// Expect the service call to reject with an McpError (likely 404)
			await expect(
				atlassianWorkspacesService.get(invalidSlug),
			).rejects.toThrow(McpError);

			// Optionally check the status code if needed
			try {
				await atlassianWorkspacesService.get(invalidSlug);
			} catch (e) {
				expect(e).toBeInstanceOf(McpError);
				expect((e as McpError).statusCode).toBe(404); // Expecting Not Found
			}
		}, 30000);
	});
});

```
Page 1/4FirstPrevNextLast