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

# Directory Structure

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

# Files

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

```typescript
  1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { z } from 'zod';
  3 | import { Logger } from '../utils/logger.util.js';
  4 | import { formatErrorForMcpTool } from '../utils/error.util.js';
  5 | import {
  6 | 	ListPullRequestsToolArgs,
  7 | 	type ListPullRequestsToolArgsType,
  8 | 	GetPullRequestToolArgs,
  9 | 	type GetPullRequestToolArgsType,
 10 | 	ListPullRequestCommentsToolArgs,
 11 | 	type ListPullRequestCommentsToolArgsType,
 12 | 	CreatePullRequestCommentToolArgs,
 13 | 	type CreatePullRequestCommentToolArgsType,
 14 | 	CreatePullRequestToolArgs,
 15 | 	type CreatePullRequestToolArgsType,
 16 | 	UpdatePullRequestToolArgs,
 17 | 	type UpdatePullRequestToolArgsType,
 18 | 	ApprovePullRequestToolArgs,
 19 | 	type ApprovePullRequestToolArgsType,
 20 | 	RejectPullRequestToolArgs,
 21 | 	type RejectPullRequestToolArgsType,
 22 | } from './atlassian.pullrequests.types.js';
 23 | import atlassianPullRequestsController from '../controllers/atlassian.pullrequests.controller.js';
 24 | 
 25 | // Create a contextualized logger for this file
 26 | const toolLogger = Logger.forContext('tools/atlassian.pullrequests.tool.ts');
 27 | 
 28 | // Log tool initialization
 29 | toolLogger.debug('Bitbucket pull requests tool initialized');
 30 | 
 31 | /**
 32 |  * MCP Tool: List Bitbucket Pull Requests
 33 |  *
 34 |  * Lists pull requests for a specific repository with optional filtering.
 35 |  * Returns a formatted markdown response with pull request details.
 36 |  *
 37 |  * @param args - Tool arguments for filtering pull requests
 38 |  * @returns MCP response with formatted pull requests list
 39 |  * @throws Will return error message if pull request listing fails
 40 |  */
 41 | async function listPullRequests(args: Record<string, unknown>) {
 42 | 	const methodLogger = Logger.forContext(
 43 | 		'tools/atlassian.pullrequests.tool.ts',
 44 | 		'listPullRequests',
 45 | 	);
 46 | 	methodLogger.debug('Listing Bitbucket pull requests with filters:', args);
 47 | 
 48 | 	try {
 49 | 		// Pass args directly to controller without any logic
 50 | 		const result = await atlassianPullRequestsController.list(
 51 | 			args as ListPullRequestsToolArgsType,
 52 | 		);
 53 | 
 54 | 		methodLogger.debug(
 55 | 			'Successfully retrieved pull requests from controller',
 56 | 		);
 57 | 
 58 | 		return {
 59 | 			content: [
 60 | 				{
 61 | 					type: 'text' as const,
 62 | 					text: result.content,
 63 | 				},
 64 | 			],
 65 | 		};
 66 | 	} catch (error) {
 67 | 		methodLogger.error('Failed to list pull requests', error);
 68 | 		return formatErrorForMcpTool(error);
 69 | 	}
 70 | }
 71 | 
 72 | /**
 73 |  * MCP Tool: Get Bitbucket Pull Request Details
 74 |  *
 75 |  * Retrieves detailed information about a specific pull request.
 76 |  * Returns a formatted markdown response with pull request details.
 77 |  *
 78 |  * @param args - Tool arguments containing the workspace, repository, and pull request identifiers
 79 |  * @returns MCP response with formatted pull request details
 80 |  * @throws Will return error message if pull request retrieval fails
 81 |  */
 82 | async function getPullRequest(args: Record<string, unknown>) {
 83 | 	const methodLogger = Logger.forContext(
 84 | 		'tools/atlassian.pullrequests.tool.ts',
 85 | 		'getPullRequest',
 86 | 	);
 87 | 	methodLogger.debug('Getting Bitbucket pull request details:', args);
 88 | 
 89 | 	try {
 90 | 		// Pass args directly to controller
 91 | 		const result = await atlassianPullRequestsController.get(
 92 | 			args as GetPullRequestToolArgsType,
 93 | 		);
 94 | 
 95 | 		methodLogger.debug(
 96 | 			'Successfully retrieved pull request details from controller',
 97 | 		);
 98 | 
 99 | 		return {
100 | 			content: [
101 | 				{
102 | 					type: 'text' as const,
103 | 					text: result.content,
104 | 				},
105 | 			],
106 | 		};
107 | 	} catch (error) {
108 | 		methodLogger.error('Failed to get pull request details', error);
109 | 		return formatErrorForMcpTool(error);
110 | 	}
111 | }
112 | 
113 | /**
114 |  * MCP Tool: List Bitbucket Pull Request Comments
115 |  *
116 |  * Lists comments for a specific pull request, including general comments and inline code comments.
117 |  * Returns a formatted markdown response with comment details.
118 |  *
119 |  * @param args - Tool arguments containing workspace, repository, and PR identifiers
120 |  * @returns MCP response with formatted pull request comments
121 |  * @throws Will return error message if comment retrieval fails
122 |  */
123 | async function listPullRequestComments(args: Record<string, unknown>) {
124 | 	const methodLogger = Logger.forContext(
125 | 		'tools/atlassian.pullrequests.tool.ts',
126 | 		'listPullRequestComments',
127 | 	);
128 | 	methodLogger.debug('Listing pull request comments:', args);
129 | 
130 | 	try {
131 | 		// Pass args directly to controller
132 | 		const result = await atlassianPullRequestsController.listComments(
133 | 			args as ListPullRequestCommentsToolArgsType,
134 | 		);
135 | 
136 | 		methodLogger.debug(
137 | 			'Successfully retrieved pull request comments from controller',
138 | 		);
139 | 
140 | 		return {
141 | 			content: [
142 | 				{
143 | 					type: 'text' as const,
144 | 					text: result.content,
145 | 				},
146 | 			],
147 | 		};
148 | 	} catch (error) {
149 | 		methodLogger.error('Failed to get pull request comments', error);
150 | 		return formatErrorForMcpTool(error);
151 | 	}
152 | }
153 | 
154 | /**
155 |  * MCP Tool: Add Bitbucket Pull Request Comment
156 |  *
157 |  * Adds a comment to a specific pull request, with support for general and inline comments.
158 |  * Returns a success message as markdown.
159 |  *
160 |  * @param args - Tool arguments containing workspace, repository, PR ID, and comment content
161 |  * @returns MCP response with formatted success message
162 |  * @throws Will return error message if comment creation fails
163 |  */
164 | async function addPullRequestComment(args: Record<string, unknown>) {
165 | 	const methodLogger = Logger.forContext(
166 | 		'tools/atlassian.pullrequests.tool.ts',
167 | 		'addPullRequestComment',
168 | 	);
169 | 	methodLogger.debug('Adding pull request comment:', {
170 | 		...args,
171 | 		content: args.content
172 | 			? `(length: ${(args.content as string).length})`
173 | 			: '(none)',
174 | 	});
175 | 
176 | 	try {
177 | 		// Pass args directly to controller
178 | 		const result = await atlassianPullRequestsController.addComment(
179 | 			args as CreatePullRequestCommentToolArgsType,
180 | 		);
181 | 
182 | 		methodLogger.debug(
183 | 			'Successfully added pull request comment via controller',
184 | 		);
185 | 
186 | 		return {
187 | 			content: [
188 | 				{
189 | 					type: 'text' as const,
190 | 					text: result.content,
191 | 				},
192 | 			],
193 | 		};
194 | 	} catch (error) {
195 | 		methodLogger.error('Failed to add pull request comment', error);
196 | 		return formatErrorForMcpTool(error);
197 | 	}
198 | }
199 | 
200 | /**
201 |  * MCP Tool: Create Bitbucket Pull Request
202 |  *
203 |  * Creates a new pull request between two branches in a Bitbucket repository.
204 |  * Returns a formatted markdown response with the newly created pull request details.
205 |  *
206 |  * @param args - Tool arguments containing workspace, repository, source branch, destination branch, and title
207 |  * @returns MCP response with formatted pull request details
208 |  * @throws Will return error message if pull request creation fails
209 |  */
210 | async function addPullRequest(args: Record<string, unknown>) {
211 | 	const methodLogger = Logger.forContext(
212 | 		'tools/atlassian.pullrequests.tool.ts',
213 | 		'addPullRequest',
214 | 	);
215 | 	methodLogger.debug('Creating new pull request:', {
216 | 		...args,
217 | 		description: args.description
218 | 			? `(length: ${(args.description as string).length})`
219 | 			: '(none)',
220 | 	});
221 | 
222 | 	try {
223 | 		// Pass args directly to controller
224 | 		const result = await atlassianPullRequestsController.add(
225 | 			args as CreatePullRequestToolArgsType,
226 | 		);
227 | 
228 | 		methodLogger.debug('Successfully created pull request via controller');
229 | 
230 | 		return {
231 | 			content: [
232 | 				{
233 | 					type: 'text' as const,
234 | 					text: result.content,
235 | 				},
236 | 			],
237 | 		};
238 | 	} catch (error) {
239 | 		methodLogger.error('Failed to create pull request', error);
240 | 		return formatErrorForMcpTool(error);
241 | 	}
242 | }
243 | 
244 | /**
245 |  * MCP Tool: Update Bitbucket Pull Request
246 |  *
247 |  * Updates an existing pull request's title and/or description.
248 |  * Returns a formatted markdown response with updated pull request details.
249 |  *
250 |  * @param args - Tool arguments for updating pull request
251 |  * @returns MCP response with formatted updated pull request details
252 |  * @throws Will return error message if pull request update fails
253 |  */
254 | async function updatePullRequest(args: Record<string, unknown>) {
255 | 	const methodLogger = Logger.forContext(
256 | 		'tools/atlassian.pullrequests.tool.ts',
257 | 		'updatePullRequest',
258 | 	);
259 | 	methodLogger.debug('Updating pull request:', {
260 | 		...args,
261 | 		description: args.description
262 | 			? `(length: ${(args.description as string).length})`
263 | 			: '(unchanged)',
264 | 	});
265 | 
266 | 	try {
267 | 		// Pass args directly to controller
268 | 		const result = await atlassianPullRequestsController.update(
269 | 			args as UpdatePullRequestToolArgsType,
270 | 		);
271 | 
272 | 		methodLogger.debug('Successfully updated pull request via controller');
273 | 
274 | 		return {
275 | 			content: [
276 | 				{
277 | 					type: 'text' as const,
278 | 					text: result.content,
279 | 				},
280 | 			],
281 | 		};
282 | 	} catch (error) {
283 | 		methodLogger.error('Failed to update pull request', error);
284 | 		return formatErrorForMcpTool(error);
285 | 	}
286 | }
287 | 
288 | /**
289 |  * MCP Tool: Approve Bitbucket Pull Request
290 |  *
291 |  * Approves a pull request, marking it as approved by the current user.
292 |  * Returns a formatted markdown response with approval confirmation.
293 |  *
294 |  * @param args - Tool arguments for approving pull request
295 |  * @returns MCP response with formatted approval confirmation
296 |  * @throws Will return error message if pull request approval fails
297 |  */
298 | async function approvePullRequest(args: Record<string, unknown>) {
299 | 	const methodLogger = Logger.forContext(
300 | 		'tools/atlassian.pullrequests.tool.ts',
301 | 		'approvePullRequest',
302 | 	);
303 | 	methodLogger.debug('Approving pull request:', args);
304 | 
305 | 	try {
306 | 		// Pass args directly to controller
307 | 		const result = await atlassianPullRequestsController.approve(
308 | 			args as ApprovePullRequestToolArgsType,
309 | 		);
310 | 
311 | 		methodLogger.debug('Successfully approved pull request via controller');
312 | 
313 | 		return {
314 | 			content: [
315 | 				{
316 | 					type: 'text' as const,
317 | 					text: result.content,
318 | 				},
319 | 			],
320 | 		};
321 | 	} catch (error) {
322 | 		methodLogger.error('Failed to approve pull request', error);
323 | 		return formatErrorForMcpTool(error);
324 | 	}
325 | }
326 | 
327 | /**
328 |  * MCP Tool: Request Changes on Bitbucket Pull Request
329 |  *
330 |  * Requests changes on a pull request, marking it as requiring changes by the current user.
331 |  * Returns a formatted markdown response with rejection confirmation.
332 |  *
333 |  * @param args - Tool arguments for requesting changes on pull request
334 |  * @returns MCP response with formatted rejection confirmation
335 |  * @throws Will return error message if pull request rejection fails
336 |  */
337 | async function rejectPullRequest(args: Record<string, unknown>) {
338 | 	const methodLogger = Logger.forContext(
339 | 		'tools/atlassian.pullrequests.tool.ts',
340 | 		'rejectPullRequest',
341 | 	);
342 | 	methodLogger.debug('Requesting changes on pull request:', args);
343 | 
344 | 	try {
345 | 		// Pass args directly to controller
346 | 		const result = await atlassianPullRequestsController.reject(
347 | 			args as RejectPullRequestToolArgsType,
348 | 		);
349 | 
350 | 		methodLogger.debug(
351 | 			'Successfully requested changes on pull request via controller',
352 | 		);
353 | 
354 | 		return {
355 | 			content: [
356 | 				{
357 | 					type: 'text' as const,
358 | 					text: result.content,
359 | 				},
360 | 			],
361 | 		};
362 | 	} catch (error) {
363 | 		methodLogger.error('Failed to request changes on pull request', error);
364 | 		return formatErrorForMcpTool(error);
365 | 	}
366 | }
367 | 
368 | /**
369 |  * Register Atlassian Pull Requests MCP Tools
370 |  *
371 |  * Registers the pull requests-related tools with the MCP server.
372 |  * Each tool is registered with its schema, description, and handler function.
373 |  *
374 |  * @param server - The MCP server instance to register tools with
375 |  */
376 | function registerTools(server: McpServer) {
377 | 	const methodLogger = Logger.forContext(
378 | 		'tools/atlassian.pullrequests.tool.ts',
379 | 		'registerTools',
380 | 	);
381 | 	methodLogger.debug('Registering Atlassian Pull Requests tools...');
382 | 
383 | 	// Register the list pull requests tool
384 | 	server.tool(
385 | 		'bb_ls_prs',
386 | 		`Lists pull requests within a repository (\`repoSlug\`). If \`workspaceSlug\` is not provided, the system will use your default workspace. Filters by \`state\` (OPEN, MERGED, DECLINED, SUPERSEDED) and supports text search via \`query\`. Supports pagination via \`limit\` and \`cursor\`. Pagination details are included at the end of the text content. Returns a formatted Markdown list with each PR's title, status, author, reviewers, and creation date. Requires Bitbucket credentials to be configured.`,
387 | 		ListPullRequestsToolArgs.shape,
388 | 		listPullRequests,
389 | 	);
390 | 
391 | 	// Register the get pull request tool
392 | 	server.tool(
393 | 		'bb_get_pr',
394 | 		`Retrieves detailed information about a specific pull request identified by \`prId\` within a repository (\`repoSlug\`). If \`workspaceSlug\` is not provided, the system will use your default workspace. Includes PR details, status, reviewers, and diff statistics. Set \`includeFullDiff\` to true (default) for the complete code changes. Set \`includeComments\` to true to also retrieve comments (default: false; Note: Enabling this may increase response time for pull requests with many comments). Returns rich information as formatted Markdown, including PR summary, code changes, and optionally comments. Requires Bitbucket credentials to be configured.`,
395 | 		GetPullRequestToolArgs.shape,
396 | 		getPullRequest,
397 | 	);
398 | 
399 | 	// Register the list pull request comments tool
400 | 	server.tool(
401 | 		'bb_ls_pr_comments',
402 | 		`Lists comments on a specific pull request identified by \`prId\` within a repository (\`repoSlug\`). If \`workspaceSlug\` is not provided, the system will use your default workspace. Retrieves both general PR comments and inline code comments, indicating their location if applicable. Supports pagination via \`limit\` and \`cursor\`. Pagination details are included at the end of the text content. Returns a formatted Markdown list with each comment's author, timestamp, content, and location for inline comments. Requires Bitbucket credentials to be configured.`,
403 | 		ListPullRequestCommentsToolArgs.shape,
404 | 		listPullRequestComments,
405 | 	);
406 | 
407 | 	// Register the add pull request comment tool
408 | 	server.tool(
409 | 		'bb_add_pr_comment',
410 | 		`Adds a comment to a specific pull request identified by \`prId\` within a repository (\`repoSlug\`). If \`workspaceSlug\` is not provided, the system will use your default workspace. The \`content\` parameter accepts Markdown-formatted text for the comment body. To reply to an existing comment, provide its ID in the \`parentId\` parameter. For inline code comments, provide both \`inline.path\` (file path) and \`inline.line\` (line number). Returns a success message as formatted Markdown. Requires Bitbucket credentials with write permissions to be configured.`,
411 | 		CreatePullRequestCommentToolArgs.shape,
412 | 		addPullRequestComment,
413 | 	);
414 | 
415 | 	// Register the create pull request tool
416 | 	// Note: Using prTitle instead of title to avoid MCP SDK conflict
417 | 	const createPrSchema = z.object({
418 | 		workspaceSlug: CreatePullRequestToolArgs.shape.workspaceSlug,
419 | 		repoSlug: CreatePullRequestToolArgs.shape.repoSlug,
420 | 		prTitle: CreatePullRequestToolArgs.shape.title, // Renamed from 'title' to 'prTitle'
421 | 		sourceBranch: CreatePullRequestToolArgs.shape.sourceBranch,
422 | 		destinationBranch: CreatePullRequestToolArgs.shape.destinationBranch,
423 | 		description: CreatePullRequestToolArgs.shape.description,
424 | 		closeSourceBranch: CreatePullRequestToolArgs.shape.closeSourceBranch,
425 | 	});
426 | 	server.tool(
427 | 		'bb_add_pr',
428 | 		`Creates a new pull request in a repository (\`repoSlug\`). If \`workspaceSlug\` is not provided, the system will use your default workspace. Required parameters include \`prTitle\` (the PR title), \`sourceBranch\` (branch with changes), and optionally \`destinationBranch\` (target branch, defaults to main/master). The \`description\` parameter accepts Markdown-formatted text for the PR description. Set \`closeSourceBranch\` to true to automatically delete the source branch after merging. Returns the newly created pull request details as formatted Markdown. Requires Bitbucket credentials with write permissions to be configured.`,
429 | 		createPrSchema.shape,
430 | 		async (args: Record<string, unknown>) => {
431 | 			// Map prTitle back to title for the controller
432 | 			const mappedArgs = { ...args, title: args.prTitle };
433 | 			delete (mappedArgs as Record<string, unknown>).prTitle;
434 | 			return addPullRequest(mappedArgs);
435 | 		},
436 | 	);
437 | 
438 | 	// Register the update pull request tool
439 | 	// Note: Using prTitle instead of title to avoid MCP SDK conflict
440 | 	const updatePrSchema = z.object({
441 | 		workspaceSlug: UpdatePullRequestToolArgs.shape.workspaceSlug,
442 | 		repoSlug: UpdatePullRequestToolArgs.shape.repoSlug,
443 | 		pullRequestId: UpdatePullRequestToolArgs.shape.pullRequestId,
444 | 		prTitle: UpdatePullRequestToolArgs.shape.title, // Renamed from 'title' to 'prTitle'
445 | 		description: UpdatePullRequestToolArgs.shape.description,
446 | 	});
447 | 	server.tool(
448 | 		'bb_update_pr',
449 | 		`Updates an existing pull request in a repository (\`repoSlug\`) identified by \`pullRequestId\`. If \`workspaceSlug\` is not provided, the system will use your default workspace. You can update the \`prTitle\` (the PR title) and/or \`description\` fields. At least one field must be provided. The \`description\` parameter accepts Markdown-formatted text. Returns the updated pull request details as formatted Markdown. Requires Bitbucket credentials with write permissions to be configured.`,
450 | 		updatePrSchema.shape,
451 | 		async (args: Record<string, unknown>) => {
452 | 			// Map prTitle back to title for the controller
453 | 			const mappedArgs = { ...args, title: args.prTitle };
454 | 			delete (mappedArgs as Record<string, unknown>).prTitle;
455 | 			return updatePullRequest(mappedArgs);
456 | 		},
457 | 	);
458 | 
459 | 	// Register the approve pull request tool
460 | 	server.tool(
461 | 		'bb_approve_pr',
462 | 		`Approves a pull request in a repository (\`repoSlug\`) identified by \`pullRequestId\`. If \`workspaceSlug\` is not provided, the system will use your default workspace. This marks the pull request as approved by the current user, indicating that the changes are ready for merge (pending any other required approvals or checks). Returns an approval confirmation as formatted Markdown. Requires Bitbucket credentials with appropriate permissions to be configured.`,
463 | 		ApprovePullRequestToolArgs.shape,
464 | 		approvePullRequest,
465 | 	);
466 | 
467 | 	// Register the reject pull request tool
468 | 	server.tool(
469 | 		'bb_reject_pr',
470 | 		`Requests changes on a pull request in a repository (\`repoSlug\`) identified by \`pullRequestId\`. If \`workspaceSlug\` is not provided, the system will use your default workspace. This marks the pull request as requiring changes by the current user, indicating that the author should address feedback before the pull request can be merged. Returns a rejection confirmation as formatted Markdown. Requires Bitbucket credentials with appropriate permissions to be configured.`,
471 | 		RejectPullRequestToolArgs.shape,
472 | 		rejectPullRequest,
473 | 	);
474 | 
475 | 	methodLogger.debug('Successfully registered Pull Requests tools');
476 | }
477 | 
478 | export default { registerTools };
479 | 
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import {
  3 | 	createAuthMissingError,
  4 | 	createApiError,
  5 | 	McpError,
  6 | } from '../utils/error.util.js';
  7 | import { Logger } from '../utils/logger.util.js';
  8 | import {
  9 | 	fetchAtlassian,
 10 | 	getAtlassianCredentials,
 11 | } from '../utils/transport.util.js';
 12 | import {
 13 | 	validatePageSize,
 14 | 	validatePaginationLimits,
 15 | } from '../utils/pagination.util.js';
 16 | import {
 17 | 	ListRepositoriesParamsSchema,
 18 | 	GetRepositoryParamsSchema,
 19 | 	ListCommitsParamsSchema,
 20 | 	RepositoriesResponseSchema,
 21 | 	RepositorySchema,
 22 | 	PaginatedCommitsSchema,
 23 | 	CreateBranchParamsSchema,
 24 | 	BranchRefSchema,
 25 | 	GetFileContentParamsSchema,
 26 | 	type ListRepositoriesParams,
 27 | 	type GetRepositoryParams,
 28 | 	type ListCommitsParams,
 29 | 	type Repository,
 30 | 	type CreateBranchParams,
 31 | 	type BranchRef,
 32 | 	type GetFileContentParams,
 33 | 	ListBranchesParamsSchema,
 34 | 	BranchesResponseSchema,
 35 | 	type ListBranchesParams,
 36 | 	type BranchesResponse,
 37 | } from './vendor.atlassian.repositories.types.js';
 38 | 
 39 | /**
 40 |  * Base API path for Bitbucket REST API v2
 41 |  * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/
 42 |  * @constant {string}
 43 |  */
 44 | const API_PATH = '/2.0';
 45 | 
 46 | /**
 47 |  * @namespace VendorAtlassianRepositoriesService
 48 |  * @description Service for interacting with Bitbucket Repositories API.
 49 |  * Provides methods for listing repositories and retrieving repository details.
 50 |  * All methods require valid Atlassian credentials configured in the environment.
 51 |  */
 52 | 
 53 | // Create a contextualized logger for this file
 54 | const serviceLogger = Logger.forContext(
 55 | 	'services/vendor.atlassian.repositories.service.ts',
 56 | );
 57 | 
 58 | // Log service initialization
 59 | serviceLogger.debug('Bitbucket repositories service initialized');
 60 | 
 61 | /**
 62 |  * List repositories for a workspace
 63 |  * @param {string} workspace - Workspace name or UUID
 64 |  * @param {ListRepositoriesParams} [params={}] - Optional parameters
 65 |  * @param {string} [params.q] - Query string to filter repositories
 66 |  * @param {string} [params.sort] - Property to sort by (e.g., 'name', '-created_on')
 67 |  * @param {number} [params.page] - Page number for pagination
 68 |  * @param {number} [params.pagelen] - Number of items per page
 69 |  * @returns {Promise<RepositoriesResponse>} Response containing repositories
 70 |  * @example
 71 |  * ```typescript
 72 |  * // List repositories in a workspace, filtered and sorted
 73 |  * const response = await listRepositories('myworkspace', {
 74 |  *   q: 'name~"api"',
 75 |  *   sort: 'name',
 76 |  *   pagelen: 25
 77 |  * });
 78 |  * ```
 79 |  */
 80 | async function list(
 81 | 	params: ListRepositoriesParams,
 82 | ): Promise<z.infer<typeof RepositoriesResponseSchema>> {
 83 | 	const methodLogger = Logger.forContext(
 84 | 		'services/vendor.atlassian.repositories.service.ts',
 85 | 		'list',
 86 | 	);
 87 | 	methodLogger.debug('Listing Bitbucket repositories with params:', params);
 88 | 
 89 | 	// Validate params with Zod
 90 | 	try {
 91 | 		ListRepositoriesParamsSchema.parse(params);
 92 | 	} catch (error) {
 93 | 		if (error instanceof z.ZodError) {
 94 | 			methodLogger.error(
 95 | 				'Invalid parameters provided to list repositories:',
 96 | 				error.format(),
 97 | 			);
 98 | 			throw createApiError(
 99 | 				`Invalid parameters: ${error.issues.map((e) => e.message).join(', ')}`,
100 | 				400,
101 | 				error,
102 | 			);
103 | 		}
104 | 		throw error;
105 | 	}
106 | 
107 | 	const credentials = getAtlassianCredentials();
108 | 	if (!credentials) {
109 | 		throw createAuthMissingError(
110 | 			'Atlassian credentials are required for this operation',
111 | 		);
112 | 	}
113 | 
114 | 	// Construct query parameters
115 | 	const queryParams = new URLSearchParams();
116 | 
117 | 	// Add optional query parameters
118 | 	if (params.q) {
119 | 		queryParams.set('q', params.q);
120 | 	}
121 | 	if (params.sort) {
122 | 		queryParams.set('sort', params.sort);
123 | 	}
124 | 	if (params.role) {
125 | 		queryParams.set('role', params.role);
126 | 	}
127 | 
128 | 	// Validate and enforce page size limits (CWE-770)
129 | 	const validatedPagelen = validatePageSize(
130 | 		params.pagelen,
131 | 		'listRepositories',
132 | 	);
133 | 	queryParams.set('pagelen', validatedPagelen.toString());
134 | 
135 | 	if (params.page) {
136 | 		queryParams.set('page', params.page.toString());
137 | 	}
138 | 
139 | 	const queryString = queryParams.toString()
140 | 		? `?${queryParams.toString()}`
141 | 		: '';
142 | 	const path = `${API_PATH}/repositories/${params.workspace}${queryString}`;
143 | 
144 | 	methodLogger.debug(`Sending request to: ${path}`);
145 | 	try {
146 | 		const rawData = await fetchAtlassian(credentials, path);
147 | 		// Validate response with Zod schema
148 | 		try {
149 | 			const validatedData = RepositoriesResponseSchema.parse(rawData);
150 | 
151 | 			// Validate pagination limits to prevent excessive data exposure (CWE-770)
152 | 			if (!validatePaginationLimits(validatedData, 'listRepositories')) {
153 | 				methodLogger.warn(
154 | 					'Response pagination exceeds configured limits',
155 | 				);
156 | 			}
157 | 
158 | 			return validatedData;
159 | 		} catch (error) {
160 | 			if (error instanceof z.ZodError) {
161 | 				methodLogger.error(
162 | 					'Invalid response from Bitbucket API:',
163 | 					error.format(),
164 | 				);
165 | 				throw createApiError(
166 | 					'Received invalid response format from Bitbucket API',
167 | 					500,
168 | 					error,
169 | 				);
170 | 			}
171 | 			throw error;
172 | 		}
173 | 	} catch (error) {
174 | 		if (error instanceof McpError) {
175 | 			throw error;
176 | 		}
177 | 		throw createApiError(
178 | 			`Failed to list repositories: ${error instanceof Error ? error.message : String(error)}`,
179 | 			500,
180 | 			error,
181 | 		);
182 | 	}
183 | }
184 | 
185 | /**
186 |  * Get detailed information about a specific Bitbucket repository
187 |  *
188 |  * Retrieves comprehensive details about a single repository.
189 |  *
190 |  * @async
191 |  * @memberof VendorAtlassianRepositoriesService
192 |  * @param {GetRepositoryParams} params - Parameters for the request
193 |  * @param {string} params.workspace - The workspace slug
194 |  * @param {string} params.repo_slug - The repository slug
195 |  * @returns {Promise<Repository>} Promise containing the detailed repository information
196 |  * @throws {Error} If Atlassian credentials are missing or API request fails
197 |  * @example
198 |  * // Get repository details
199 |  * const repository = await get({
200 |  *   workspace: 'my-workspace',
201 |  *   repo_slug: 'my-repo'
202 |  * });
203 |  */
204 | async function get(params: GetRepositoryParams): Promise<Repository> {
205 | 	const methodLogger = Logger.forContext(
206 | 		'services/vendor.atlassian.repositories.service.ts',
207 | 		'get',
208 | 	);
209 | 	methodLogger.debug(
210 | 		`Getting Bitbucket repository: ${params.workspace}/${params.repo_slug}`,
211 | 	);
212 | 
213 | 	// Validate params with Zod
214 | 	try {
215 | 		GetRepositoryParamsSchema.parse(params);
216 | 	} catch (error) {
217 | 		if (error instanceof z.ZodError) {
218 | 			methodLogger.error(
219 | 				'Invalid parameters provided to get repository:',
220 | 				error.format(),
221 | 			);
222 | 			throw createApiError(
223 | 				`Invalid parameters: ${error.issues.map((e) => e.message).join(', ')}`,
224 | 				400,
225 | 				error,
226 | 			);
227 | 		}
228 | 		throw error;
229 | 	}
230 | 
231 | 	const credentials = getAtlassianCredentials();
232 | 	if (!credentials) {
233 | 		throw createAuthMissingError(
234 | 			'Atlassian credentials are required for this operation',
235 | 		);
236 | 	}
237 | 
238 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}`;
239 | 
240 | 	methodLogger.debug(`Sending request to: ${path}`);
241 | 	try {
242 | 		const rawData = await fetchAtlassian(credentials, path);
243 | 
244 | 		// Validate response with Zod schema
245 | 		try {
246 | 			const validatedData = RepositorySchema.parse(rawData);
247 | 			return validatedData;
248 | 		} catch (error) {
249 | 			if (error instanceof z.ZodError) {
250 | 				// Log the detailed formatting errors but provide a clear message to users
251 | 				methodLogger.error(
252 | 					'Bitbucket API response validation failed:',
253 | 					error.format(),
254 | 				);
255 | 
256 | 				// Create API error with appropriate context for validation failures
257 | 				throw createApiError(
258 | 					`Invalid response format from Bitbucket API for repository ${params.workspace}/${params.repo_slug}`,
259 | 					500, // Internal server error since the API responded but with unexpected format
260 | 					error, // Include the Zod error as originalError for better debugging
261 | 				);
262 | 			}
263 | 			throw error; // Re-throw any other errors
264 | 		}
265 | 	} catch (error) {
266 | 		// If it's already an McpError (from fetchAtlassian or Zod validation), just rethrow it
267 | 		if (error instanceof McpError) {
268 | 			throw error;
269 | 		}
270 | 
271 | 		// Otherwise, wrap in a standard API error with context
272 | 		throw createApiError(
273 | 			`Failed to get repository details for ${params.workspace}/${params.repo_slug}: ${error instanceof Error ? error.message : String(error)}`,
274 | 			500,
275 | 			error,
276 | 		);
277 | 	}
278 | }
279 | 
280 | /**
281 |  * Lists commits for a specific repository and optional revision/path.
282 |  *
283 |  * @param params Parameters including workspace, repo slug, and optional filters.
284 |  * @returns Promise resolving to paginated commit data.
285 |  * @throws {Error} If workspace or repo_slug are missing, or if credentials are not found.
286 |  */
287 | async function listCommits(
288 | 	params: ListCommitsParams,
289 | ): Promise<z.infer<typeof PaginatedCommitsSchema>> {
290 | 	const methodLogger = Logger.forContext(
291 | 		'services/vendor.atlassian.repositories.service.ts',
292 | 		'listCommits',
293 | 	);
294 | 	methodLogger.debug(
295 | 		`Listing commits for ${params.workspace}/${params.repo_slug}`,
296 | 		params,
297 | 	);
298 | 
299 | 	// Validate params with Zod
300 | 	try {
301 | 		ListCommitsParamsSchema.parse(params);
302 | 	} catch (error) {
303 | 		if (error instanceof z.ZodError) {
304 | 			methodLogger.error(
305 | 				'Invalid parameters provided to list commits:',
306 | 				error.format(),
307 | 			);
308 | 			throw createApiError(
309 | 				`Invalid parameters: ${error.issues.map((e) => e.message).join(', ')}`,
310 | 				400,
311 | 				error,
312 | 			);
313 | 		}
314 | 		throw error;
315 | 	}
316 | 
317 | 	const credentials = getAtlassianCredentials();
318 | 	if (!credentials) {
319 | 		throw createAuthMissingError(
320 | 			'Atlassian credentials are required for this operation',
321 | 		);
322 | 	}
323 | 
324 | 	const queryParams = new URLSearchParams();
325 | 	if (params.include) {
326 | 		queryParams.set('include', params.include);
327 | 	}
328 | 	if (params.exclude) {
329 | 		queryParams.set('exclude', params.exclude);
330 | 	}
331 | 	if (params.path) {
332 | 		queryParams.set('path', params.path);
333 | 	}
334 | 	if (params.pagelen) {
335 | 		queryParams.set('pagelen', params.pagelen.toString());
336 | 	}
337 | 	if (params.page) {
338 | 		queryParams.set('page', params.page.toString());
339 | 	}
340 | 
341 | 	const queryString = queryParams.toString()
342 | 		? `?${queryParams.toString()}`
343 | 		: '';
344 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/commits${queryString}`;
345 | 
346 | 	methodLogger.debug(`Sending commit history request to: ${path}`);
347 | 	try {
348 | 		const rawData = await fetchAtlassian(credentials, path);
349 | 		// Validate response with Zod schema
350 | 		try {
351 | 			const validatedData = PaginatedCommitsSchema.parse(rawData);
352 | 			return validatedData;
353 | 		} catch (error) {
354 | 			if (error instanceof z.ZodError) {
355 | 				methodLogger.error(
356 | 					'Invalid response from Bitbucket API:',
357 | 					error.format(),
358 | 				);
359 | 				throw createApiError(
360 | 					'Received invalid response format from Bitbucket API',
361 | 					500,
362 | 					error,
363 | 				);
364 | 			}
365 | 			throw error;
366 | 		}
367 | 	} catch (error) {
368 | 		if (error instanceof McpError) {
369 | 			throw error;
370 | 		}
371 | 		throw createApiError(
372 | 			`Failed to list commits: ${error instanceof Error ? error.message : String(error)}`,
373 | 			500,
374 | 			error,
375 | 		);
376 | 	}
377 | }
378 | 
379 | /**
380 |  * Creates a new branch in the specified repository.
381 |  *
382 |  * @param params Parameters including workspace, repo slug, new branch name, and source target.
383 |  * @returns Promise resolving to details about the newly created branch reference.
384 |  * @throws {Error} If required parameters are missing or API request fails.
385 |  */
386 | async function createBranch(params: CreateBranchParams): Promise<BranchRef> {
387 | 	const methodLogger = Logger.forContext(
388 | 		'services/vendor.atlassian.repositories.service.ts',
389 | 		'createBranch',
390 | 	);
391 | 	methodLogger.debug(
392 | 		`Creating branch '${params.name}' from target '${params.target.hash}' in ${params.workspace}/${params.repo_slug}`,
393 | 	);
394 | 
395 | 	// Validate params with Zod
396 | 	try {
397 | 		CreateBranchParamsSchema.parse(params);
398 | 	} catch (error) {
399 | 		if (error instanceof z.ZodError) {
400 | 			methodLogger.error('Invalid parameters provided:', error.format());
401 | 			throw createApiError(
402 | 				`Invalid parameters: ${error.issues.map((e) => e.message).join(', ')}`,
403 | 				400,
404 | 				error,
405 | 			);
406 | 		}
407 | 		throw error;
408 | 	}
409 | 
410 | 	const credentials = getAtlassianCredentials();
411 | 	if (!credentials) {
412 | 		throw createAuthMissingError(
413 | 			'Atlassian credentials are required for this operation',
414 | 		);
415 | 	}
416 | 
417 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/refs/branches`;
418 | 
419 | 	const requestBody = {
420 | 		name: params.name,
421 | 		target: {
422 | 			hash: params.target.hash,
423 | 		},
424 | 	};
425 | 
426 | 	methodLogger.debug(`Sending POST request to: ${path}`);
427 | 	try {
428 | 		const rawData = await fetchAtlassian<BranchRef>(credentials, path, {
429 | 			method: 'POST',
430 | 			body: requestBody,
431 | 		});
432 | 
433 | 		// Validate response with Zod schema
434 | 		try {
435 | 			const validatedData = BranchRefSchema.parse(rawData);
436 | 			methodLogger.debug('Branch created successfully:', validatedData);
437 | 			return validatedData;
438 | 		} catch (error) {
439 | 			if (error instanceof z.ZodError) {
440 | 				methodLogger.error(
441 | 					'Invalid response from Bitbucket API:',
442 | 					error.format(),
443 | 				);
444 | 				throw createApiError(
445 | 					'Received invalid response format from Bitbucket API',
446 | 					500,
447 | 					error,
448 | 				);
449 | 			}
450 | 			throw error;
451 | 		}
452 | 	} catch (error) {
453 | 		if (error instanceof McpError) {
454 | 			throw error;
455 | 		}
456 | 		throw createApiError(
457 | 			`Failed to create branch: ${error instanceof Error ? error.message : String(error)}`,
458 | 			500,
459 | 			error,
460 | 		);
461 | 	}
462 | }
463 | 
464 | /**
465 |  * Get the content of a file from a repository.
466 |  *
467 |  * This retrieves the raw content of a file at the specified path from a repository at a specific commit.
468 |  *
469 |  * @param {GetFileContentParams} params - Parameters for the request
470 |  * @param {string} params.workspace - The workspace slug or UUID
471 |  * @param {string} params.repo_slug - The repository slug or UUID
472 |  * @param {string} params.commit - The commit, branch name, or tag to get the file from
473 |  * @param {string} params.path - The file path within the repository
474 |  * @returns {Promise<string>} Promise containing the file content as a string
475 |  * @throws {Error} If parameters are invalid, credentials are missing, or API request fails
476 |  * @example
477 |  * // Get README.md content from the main branch
478 |  * const fileContent = await getFileContent({
479 |  *   workspace: 'my-workspace',
480 |  *   repo_slug: 'my-repo',
481 |  *   commit: 'main',
482 |  *   path: 'README.md'
483 |  * });
484 |  */
485 | async function getFileContent(params: GetFileContentParams): Promise<string> {
486 | 	const methodLogger = Logger.forContext(
487 | 		'services/vendor.atlassian.repositories.service.ts',
488 | 		'getFileContent',
489 | 	);
490 | 	methodLogger.debug(
491 | 		`Getting file content from ${params.workspace}/${params.repo_slug}/${params.commit}/${params.path}`,
492 | 	);
493 | 
494 | 	// Validate params with Zod
495 | 	try {
496 | 		GetFileContentParamsSchema.parse(params);
497 | 	} catch (error) {
498 | 		if (error instanceof z.ZodError) {
499 | 			methodLogger.error(
500 | 				'Invalid parameters provided to get file content:',
501 | 				error.format(),
502 | 			);
503 | 			throw createApiError(
504 | 				`Invalid parameters: ${error.issues.map((e) => e.message).join(', ')}`,
505 | 				400,
506 | 				error,
507 | 			);
508 | 		}
509 | 		throw error;
510 | 	}
511 | 
512 | 	const credentials = getAtlassianCredentials();
513 | 	if (!credentials) {
514 | 		throw createAuthMissingError(
515 | 			'Atlassian credentials are required for this operation',
516 | 		);
517 | 	}
518 | 
519 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/src/${params.commit}/${params.path}`;
520 | 
521 | 	methodLogger.debug(`Sending request to: ${path}`);
522 | 	try {
523 | 		// Use fetchAtlassian to get the file content directly as string
524 | 		// The function already detects text/plain content type and returns it appropriately
525 | 		const fileContent = await fetchAtlassian<string>(credentials, path);
526 | 
527 | 		methodLogger.debug(
528 | 			`Successfully retrieved file content (${fileContent.length} characters)`,
529 | 		);
530 | 		return fileContent;
531 | 	} catch (error) {
532 | 		if (error instanceof McpError) {
533 | 			throw error;
534 | 		}
535 | 
536 | 		// More specific error messages for common file issues
537 | 		if (error instanceof Error && error.message.includes('404')) {
538 | 			throw createApiError(
539 | 				`File not found: ${params.path} at ${params.commit}`,
540 | 				404,
541 | 				error,
542 | 			);
543 | 		}
544 | 
545 | 		throw createApiError(
546 | 			`Failed to get file content: ${error instanceof Error ? error.message : String(error)}`,
547 | 			500,
548 | 			error,
549 | 		);
550 | 	}
551 | }
552 | 
553 | /**
554 |  * Lists branches for a specific repository.
555 |  *
556 |  * @param params Parameters including workspace, repo slug, and optional filters.
557 |  * @returns Promise resolving to paginated branches data.
558 |  * @throws {Error} If workspace or repo_slug are missing, or if credentials are not found.
559 |  */
560 | async function listBranches(
561 | 	params: ListBranchesParams,
562 | ): Promise<BranchesResponse> {
563 | 	const methodLogger = Logger.forContext(
564 | 		'services/vendor.atlassian.repositories.service.ts',
565 | 		'listBranches',
566 | 	);
567 | 	methodLogger.debug(
568 | 		`Listing branches for ${params.workspace}/${params.repo_slug}`,
569 | 		params,
570 | 	);
571 | 
572 | 	// Validate params with Zod
573 | 	try {
574 | 		ListBranchesParamsSchema.parse(params);
575 | 	} catch (error) {
576 | 		if (error instanceof z.ZodError) {
577 | 			methodLogger.error(
578 | 				'Invalid parameters provided to list branches:',
579 | 				error.format(),
580 | 			);
581 | 			throw createApiError(
582 | 				`Invalid parameters: ${error.issues.map((e) => e.message).join(', ')}`,
583 | 				400,
584 | 				error,
585 | 			);
586 | 		}
587 | 		throw error;
588 | 	}
589 | 
590 | 	const credentials = getAtlassianCredentials();
591 | 	if (!credentials) {
592 | 		throw createAuthMissingError(
593 | 			'Atlassian credentials are required for this operation',
594 | 		);
595 | 	}
596 | 
597 | 	const queryParams = new URLSearchParams();
598 | 	if (params.q) {
599 | 		queryParams.set('q', params.q);
600 | 	}
601 | 	if (params.sort) {
602 | 		queryParams.set('sort', params.sort);
603 | 	}
604 | 	if (params.pagelen) {
605 | 		queryParams.set('pagelen', params.pagelen.toString());
606 | 	}
607 | 	if (params.page) {
608 | 		queryParams.set('page', params.page.toString());
609 | 	}
610 | 
611 | 	const queryString = queryParams.toString()
612 | 		? `?${queryParams.toString()}`
613 | 		: '';
614 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/refs/branches${queryString}`;
615 | 
616 | 	methodLogger.debug(`Sending branches request to: ${path}`);
617 | 	try {
618 | 		const rawData = await fetchAtlassian(credentials, path);
619 | 		// Validate response with Zod schema
620 | 		try {
621 | 			const validatedData = BranchesResponseSchema.parse(rawData);
622 | 			return validatedData;
623 | 		} catch (error) {
624 | 			if (error instanceof z.ZodError) {
625 | 				methodLogger.error(
626 | 					'Invalid response from Bitbucket API:',
627 | 					error.format(),
628 | 				);
629 | 				throw createApiError(
630 | 					'Received invalid response format from Bitbucket API',
631 | 					500,
632 | 					error,
633 | 				);
634 | 			}
635 | 			throw error;
636 | 		}
637 | 	} catch (error) {
638 | 		if (error instanceof McpError) {
639 | 			throw error;
640 | 		}
641 | 		throw createApiError(
642 | 			`Failed to list branches: ${error instanceof Error ? error.message : String(error)}`,
643 | 			500,
644 | 			error,
645 | 		);
646 | 	}
647 | }
648 | 
649 | export default {
650 | 	list,
651 | 	get,
652 | 	listCommits,
653 | 	createBranch,
654 | 	getFileContent,
655 | 	listBranches,
656 | };
657 | 
```

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

```typescript
  1 | import { Command } from 'commander';
  2 | import { Logger } from '../utils/logger.util.js';
  3 | import { handleCliError } from '../utils/error.util.js';
  4 | import atlassianPullRequestsController from '../controllers/atlassian.pullrequests.controller.js';
  5 | 
  6 | /**
  7 |  * CLI module for managing Bitbucket pull requests.
  8 |  * Provides commands for listing, retrieving, and manipulating pull requests.
  9 |  * All commands require valid Atlassian credentials.
 10 |  */
 11 | 
 12 | // Create a contextualized logger for this file
 13 | const cliLogger = Logger.forContext('cli/atlassian.pullrequests.cli.ts');
 14 | 
 15 | // Log CLI initialization
 16 | cliLogger.debug('Bitbucket pull requests CLI module initialized');
 17 | 
 18 | /**
 19 |  * Register Bitbucket pull requests CLI commands with the Commander program
 20 |  * @param program - The Commander program instance to register commands with
 21 |  * @throws Error if command registration fails
 22 |  */
 23 | function register(program: Command): void {
 24 | 	const methodLogger = Logger.forContext(
 25 | 		'cli/atlassian.pullrequests.cli.ts',
 26 | 		'register',
 27 | 	);
 28 | 	methodLogger.debug('Registering Bitbucket Pull Requests CLI commands...');
 29 | 
 30 | 	registerListPullRequestsCommand(program);
 31 | 	registerGetPullRequestCommand(program);
 32 | 	registerListPullRequestCommentsCommand(program);
 33 | 	registerAddPullRequestCommentCommand(program);
 34 | 	registerAddPullRequestCommand(program);
 35 | 	registerUpdatePullRequestCommand(program);
 36 | 	registerApprovePullRequestCommand(program);
 37 | 	registerRejectPullRequestCommand(program);
 38 | 
 39 | 	methodLogger.debug('CLI commands registered successfully');
 40 | }
 41 | 
 42 | /**
 43 |  * Register the command for listing Bitbucket pull requests
 44 |  * @param program - The Commander program instance
 45 |  */
 46 | function registerListPullRequestsCommand(program: Command): void {
 47 | 	program
 48 | 		.command('ls-prs')
 49 | 		.description(
 50 | 			'List pull requests in a Bitbucket repository, with filtering and pagination.',
 51 | 		)
 52 | 		.option(
 53 | 			'-w, --workspace-slug <slug>',
 54 | 			'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"',
 55 | 		)
 56 | 		.requiredOption(
 57 | 			'-r, --repo-slug <slug>',
 58 | 			'Repository slug containing the pull requests. This must be a valid repository in the specified workspace. Example: "project-api"',
 59 | 		)
 60 | 		.option(
 61 | 			'-s, --state <state>',
 62 | 			'Filter by pull request state: "OPEN", "MERGED", "DECLINED", or "SUPERSEDED". If omitted, returns pull requests in all states.',
 63 | 		)
 64 | 		.option(
 65 | 			'-q, --query <string>',
 66 | 			'Filter pull requests by query string. Searches pull request title and description.',
 67 | 		)
 68 | 		.option(
 69 | 			'-l, --limit <number>',
 70 | 			'Maximum number of items to return (1-100). Defaults to 25 if omitted.',
 71 | 		)
 72 | 		.option(
 73 | 			'-c, --cursor <string>',
 74 | 			'Pagination cursor for retrieving the next set of results.',
 75 | 		)
 76 | 		.action(async (options) => {
 77 | 			const actionLogger = Logger.forContext(
 78 | 				'cli/atlassian.pullrequests.cli.ts',
 79 | 				'ls-prs',
 80 | 			);
 81 | 			try {
 82 | 				actionLogger.debug('Processing command options:', options);
 83 | 
 84 | 				// Map CLI options to controller params - keep only type conversions
 85 | 				const filterOptions = {
 86 | 					workspaceSlug: options.workspaceSlug,
 87 | 					repoSlug: options.repoSlug,
 88 | 					state: options.state as
 89 | 						| 'OPEN'
 90 | 						| 'MERGED'
 91 | 						| 'DECLINED'
 92 | 						| 'SUPERSEDED'
 93 | 						| undefined,
 94 | 					query: options.query,
 95 | 					limit: options.limit
 96 | 						? parseInt(options.limit, 10)
 97 | 						: undefined,
 98 | 					cursor: options.cursor,
 99 | 				};
100 | 
101 | 				actionLogger.debug(
102 | 					'Fetching pull requests with filters:',
103 | 					filterOptions,
104 | 				);
105 | 				const result =
106 | 					await atlassianPullRequestsController.list(filterOptions);
107 | 				actionLogger.debug('Successfully retrieved pull requests');
108 | 
109 | 				// Display the content which now includes pagination information
110 | 				console.log(result.content);
111 | 			} catch (error) {
112 | 				actionLogger.error('Operation failed:', error);
113 | 				handleCliError(error);
114 | 			}
115 | 		});
116 | }
117 | 
118 | /**
119 |  * Register the command for retrieving a specific Bitbucket pull request
120 |  * @param program - The Commander program instance
121 |  */
122 | function registerGetPullRequestCommand(program: Command): void {
123 | 	program
124 | 		.command('get-pr')
125 | 		.description(
126 | 			'Get detailed information about a specific Bitbucket pull request.',
127 | 		)
128 | 		.option(
129 | 			'-w, --workspace-slug <slug>',
130 | 			'Workspace slug containing the repository. If not provided, uses your default workspace. Example: "myteam"',
131 | 		)
132 | 		.requiredOption(
133 | 			'-r, --repo-slug <slug>',
134 | 			'Repository slug containing the pull request. Must be a valid repository in the specified workspace. Example: "project-api"',
135 | 		)
136 | 		.requiredOption(
137 | 			'-p, --pr-id <id>',
138 | 			'Numeric ID of the pull request to retrieve. Must be a valid pull request ID in the specified repository. Example: "42"',
139 | 		)
140 | 		.option(
141 | 			'--include-full-diff',
142 | 			'Retrieve the full diff content instead of just the summary. Default: true (rich output by default)',
143 | 			true,
144 | 		)
145 | 		.option(
146 | 			'--include-comments',
147 | 			'Retrieve comments for the pull request. Default: false. Note: Enabling this may increase response time for pull requests with many comments due to additional API calls',
148 | 			false,
149 | 		)
150 | 		.action(async (options) => {
151 | 			const actionLogger = Logger.forContext(
152 | 				'cli/atlassian.pullrequests.cli.ts',
153 | 				'get-pr',
154 | 			);
155 | 			try {
156 | 				actionLogger.debug('Processing command options:', options);
157 | 
158 | 				// Map CLI options to controller params
159 | 				const params = {
160 | 					workspaceSlug: options.workspaceSlug,
161 | 					repoSlug: options.repoSlug,
162 | 					prId: options.prId,
163 | 					includeFullDiff: options.includeFullDiff,
164 | 					includeComments: options.includeComments,
165 | 				};
166 | 
167 | 				actionLogger.debug('Fetching pull request:', params);
168 | 				const result =
169 | 					await atlassianPullRequestsController.get(params);
170 | 				actionLogger.debug('Successfully retrieved pull request');
171 | 
172 | 				console.log(result.content);
173 | 			} catch (error) {
174 | 				actionLogger.error('Operation failed:', error);
175 | 				handleCliError(error);
176 | 			}
177 | 		});
178 | }
179 | 
180 | /**
181 |  * Register the command for listing comments on a Bitbucket pull request
182 |  * @param program - The Commander program instance
183 |  */
184 | function registerListPullRequestCommentsCommand(program: Command): void {
185 | 	program
186 | 		.command('ls-pr-comments')
187 | 		.description(
188 | 			'List comments on a specific Bitbucket pull request, with pagination.',
189 | 		)
190 | 		.option(
191 | 			'-w, --workspace-slug <slug>',
192 | 			'Workspace slug containing the repository. If not provided, uses your default workspace. Example: "myteam"',
193 | 		)
194 | 		.requiredOption(
195 | 			'-r, --repo-slug <slug>',
196 | 			'Repository slug containing the pull request. Must be a valid repository in the specified workspace. Example: "project-api"',
197 | 		)
198 | 		.requiredOption(
199 | 			'-p, --pr-id <id>',
200 | 			'Numeric ID of the pull request to retrieve comments from. Must be a valid pull request ID in the specified repository. Example: "42"',
201 | 		)
202 | 		.option(
203 | 			'-l, --limit <number>',
204 | 			'Maximum number of items to return (1-100). Defaults to 25 if omitted.',
205 | 		)
206 | 		.option(
207 | 			'-c, --cursor <string>',
208 | 			'Pagination cursor for retrieving the next set of results.',
209 | 		)
210 | 		.action(async (options) => {
211 | 			const actionLogger = Logger.forContext(
212 | 				'cli/atlassian.pullrequests.cli.ts',
213 | 				'ls-pr-comments',
214 | 			);
215 | 			try {
216 | 				actionLogger.debug('Processing command options:', options);
217 | 
218 | 				// Map CLI options to controller params - keep only type conversions
219 | 				const params = {
220 | 					workspaceSlug: options.workspaceSlug,
221 | 					repoSlug: options.repoSlug,
222 | 					prId: options.prId,
223 | 					limit: options.limit
224 | 						? parseInt(options.limit, 10)
225 | 						: undefined,
226 | 					cursor: options.cursor,
227 | 				};
228 | 
229 | 				actionLogger.debug('Fetching pull request comments:', params);
230 | 				const result =
231 | 					await atlassianPullRequestsController.listComments(params);
232 | 				actionLogger.debug(
233 | 					'Successfully retrieved pull request comments',
234 | 				);
235 | 
236 | 				// Display the content which now includes pagination information
237 | 				console.log(result.content);
238 | 			} catch (error) {
239 | 				actionLogger.error('Operation failed:', error);
240 | 				handleCliError(error);
241 | 			}
242 | 		});
243 | }
244 | 
245 | /**
246 |  * Register the command for adding a comment to a pull request
247 |  * @param program - The Commander program instance
248 |  */
249 | function registerAddPullRequestCommentCommand(program: Command): void {
250 | 	program
251 | 		.command('add-pr-comment')
252 | 		.description('Add a comment to a Bitbucket pull request.')
253 | 		.option(
254 | 			'-w, --workspace-slug <slug>',
255 | 			'Workspace slug containing the repository. If not provided, uses your default workspace. Example: "myteam"',
256 | 		)
257 | 		.requiredOption(
258 | 			'-r, --repo-slug <slug>',
259 | 			'Repository slug containing the pull request. Must be a valid repository in the specified workspace. Example: "project-api"',
260 | 		)
261 | 		.requiredOption(
262 | 			'-p, --pr-id <id>',
263 | 			'Numeric ID of the pull request to add a comment to. Must be a valid pull request ID in the specified repository. Example: "42"',
264 | 		)
265 | 		.requiredOption(
266 | 			'-m, --content <text>',
267 | 			'The content of the comment to add to the pull request. Can include markdown formatting.',
268 | 		)
269 | 		.option(
270 | 			'-f, --path <file-path>',
271 | 			'Optional: The file path to add an inline comment to.',
272 | 		)
273 | 		.option(
274 | 			'-L, --line <line-number>',
275 | 			'Optional: The line number to add the inline comment to.',
276 | 			parseInt,
277 | 		)
278 | 		.option(
279 | 			'--parent-id <id>',
280 | 			'Optional: The ID of the parent comment to reply to. If provided, this comment will be a reply to the specified comment.',
281 | 		)
282 | 		.action(async (options) => {
283 | 			const actionLogger = Logger.forContext(
284 | 				'cli/atlassian.pullrequests.cli.ts',
285 | 				'add-pr-comment',
286 | 			);
287 | 			try {
288 | 				actionLogger.debug('Processing command options:', options);
289 | 
290 | 				// Build parameters object
291 | 				const params: {
292 | 					workspaceSlug?: string;
293 | 					repoSlug: string;
294 | 					prId: string;
295 | 					content: string;
296 | 					inline?: {
297 | 						path: string;
298 | 						line: number;
299 | 					};
300 | 					parentId?: string;
301 | 				} = {
302 | 					workspaceSlug: options.workspaceSlug,
303 | 					repoSlug: options.repoSlug,
304 | 					prId: options.prId,
305 | 					content: options.content,
306 | 				};
307 | 
308 | 				// Add inline comment details if both path and line are provided
309 | 				if (options.path && options.line) {
310 | 					params.inline = {
311 | 						path: options.path,
312 | 						line: options.line,
313 | 					};
314 | 				} else if (options.path || options.line) {
315 | 					throw new Error(
316 | 						'Both -f/--path and -L/--line are required for inline comments',
317 | 					);
318 | 				}
319 | 
320 | 				// Add parent ID if provided
321 | 				if (options.parentId) {
322 | 					params.parentId = options.parentId;
323 | 				}
324 | 
325 | 				actionLogger.debug('Creating pull request comment:', {
326 | 					...params,
327 | 					content: '(content length: ' + options.content.length + ')',
328 | 				});
329 | 				const result =
330 | 					await atlassianPullRequestsController.addComment(params);
331 | 				actionLogger.debug('Successfully created pull request comment');
332 | 
333 | 				console.log(result.content);
334 | 			} catch (error) {
335 | 				actionLogger.error('Operation failed:', error);
336 | 				handleCliError(error);
337 | 			}
338 | 		});
339 | }
340 | 
341 | /**
342 |  * Register the command for adding a new pull request
343 |  * @param program - The Commander program instance
344 |  */
345 | function registerAddPullRequestCommand(program: Command): void {
346 | 	program
347 | 		.command('add-pr')
348 | 		.description('Add a new pull request in a Bitbucket repository.')
349 | 		.option(
350 | 			'-w, --workspace-slug <slug>',
351 | 			'Workspace slug containing the repository. If not provided, uses your default workspace. Example: "myteam"',
352 | 		)
353 | 		.requiredOption(
354 | 			'-r, --repo-slug <slug>',
355 | 			'Repository slug to create the pull request in. Must be a valid repository in the specified workspace. Example: "project-api"',
356 | 		)
357 | 		.requiredOption(
358 | 			'-t, --title <title>',
359 | 			'Title for the pull request. Example: "Add new feature"',
360 | 		)
361 | 		.requiredOption(
362 | 			'-s, --source-branch <branch>',
363 | 			'Source branch name (the branch containing your changes). Example: "feature/new-login"',
364 | 		)
365 | 		.option(
366 | 			'-d, --destination-branch <branch>',
367 | 			'Destination branch name (the branch you want to merge into, defaults to main). Example: "develop"',
368 | 		)
369 | 		.option(
370 | 			'--description <text>',
371 | 			'Optional description for the pull request.',
372 | 		)
373 | 		.option(
374 | 			'--close-source-branch',
375 | 			'Whether to close the source branch after the pull request is merged. Default: false',
376 | 			false,
377 | 		)
378 | 		.action(async (options) => {
379 | 			const actionLogger = Logger.forContext(
380 | 				'cli/atlassian.pullrequests.cli.ts',
381 | 				'add-pr',
382 | 			);
383 | 			try {
384 | 				actionLogger.debug('Processing command options:', options);
385 | 
386 | 				// Map CLI options to controller params
387 | 				const params = {
388 | 					workspaceSlug: options.workspaceSlug,
389 | 					repoSlug: options.repoSlug,
390 | 					title: options.title,
391 | 					sourceBranch: options.sourceBranch,
392 | 					destinationBranch: options.destinationBranch,
393 | 					description: options.description,
394 | 					closeSourceBranch: options.closeSourceBranch,
395 | 				};
396 | 
397 | 				actionLogger.debug('Creating pull request:', {
398 | 					...params,
399 | 					description: options.description
400 | 						? '(description provided)'
401 | 						: '(no description)',
402 | 				});
403 | 				const result =
404 | 					await atlassianPullRequestsController.add(params);
405 | 				actionLogger.debug('Successfully created pull request');
406 | 
407 | 				console.log(result.content);
408 | 			} catch (error) {
409 | 				actionLogger.error('Operation failed:', error);
410 | 				handleCliError(error);
411 | 			}
412 | 		});
413 | }
414 | 
415 | /**
416 |  * Register the command for updating a Bitbucket pull request
417 |  * @param program - The Commander program instance
418 |  */
419 | function registerUpdatePullRequestCommand(program: Command): void {
420 | 	program
421 | 		.command('update-pr')
422 | 		.description(
423 | 			'Update an existing pull request in a Bitbucket repository.',
424 | 		)
425 | 		.option(
426 | 			'-w, --workspace-slug <slug>',
427 | 			'Workspace slug containing the repository (optional, uses default workspace if not provided). Example: "myteam"',
428 | 		)
429 | 		.requiredOption(
430 | 			'-r, --repo-slug <slug>',
431 | 			'Repository slug containing the pull request. Example: "project-api"',
432 | 		)
433 | 		.requiredOption(
434 | 			'-p, --pull-request-id <id>',
435 | 			'Pull request ID to update. Example: 123',
436 | 			parseInt,
437 | 		)
438 | 		.option(
439 | 			'-t, --title <title>',
440 | 			'Updated title for the pull request. Example: "Updated Feature Implementation"',
441 | 		)
442 | 		.option(
443 | 			'--description <text>',
444 | 			'Updated description for the pull request.',
445 | 		)
446 | 		.action(async (options) => {
447 | 			const actionLogger = Logger.forContext(
448 | 				'cli/atlassian.pullrequests.cli.ts',
449 | 				'update-pr',
450 | 			);
451 | 			try {
452 | 				actionLogger.debug('Processing command options:', options);
453 | 
454 | 				// Validate that at least one field to update is provided
455 | 				if (!options.title && !options.description) {
456 | 					throw new Error(
457 | 						'At least one field to update (title or description) must be provided',
458 | 					);
459 | 				}
460 | 
461 | 				// Map CLI options to controller params
462 | 				const params = {
463 | 					workspaceSlug: options.workspaceSlug,
464 | 					repoSlug: options.repoSlug,
465 | 					pullRequestId: options.pullRequestId,
466 | 					title: options.title,
467 | 					description: options.description,
468 | 				};
469 | 
470 | 				actionLogger.debug('Updating pull request:', {
471 | 					...params,
472 | 					description: options.description
473 | 						? '(description provided)'
474 | 						: '(no description)',
475 | 				});
476 | 				const result =
477 | 					await atlassianPullRequestsController.update(params);
478 | 				actionLogger.debug('Successfully updated pull request');
479 | 
480 | 				console.log(result.content);
481 | 			} catch (error) {
482 | 				actionLogger.error('Operation failed:', error);
483 | 				handleCliError(error);
484 | 			}
485 | 		});
486 | }
487 | 
488 | /**
489 |  * Register the command for approving a Bitbucket pull request
490 |  * @param program - The Commander program instance
491 |  */
492 | function registerApprovePullRequestCommand(program: Command): void {
493 | 	program
494 | 		.command('approve-pr')
495 | 		.description('Approve a pull request in a Bitbucket repository.')
496 | 		.option(
497 | 			'-w, --workspace-slug <slug>',
498 | 			'Workspace slug containing the repository (optional, uses default workspace if not provided). Example: "myteam"',
499 | 		)
500 | 		.requiredOption(
501 | 			'-r, --repo-slug <slug>',
502 | 			'Repository slug containing the pull request. Example: "project-api"',
503 | 		)
504 | 		.requiredOption(
505 | 			'-p, --pull-request-id <id>',
506 | 			'Pull request ID to approve. Example: 123',
507 | 			parseInt,
508 | 		)
509 | 		.action(async (options) => {
510 | 			const actionLogger = Logger.forContext(
511 | 				'cli/atlassian.pullrequests.cli.ts',
512 | 				'approve-pr',
513 | 			);
514 | 			try {
515 | 				actionLogger.debug('Processing command options:', options);
516 | 
517 | 				// Map CLI options to controller params
518 | 				const params = {
519 | 					workspaceSlug: options.workspaceSlug,
520 | 					repoSlug: options.repoSlug,
521 | 					pullRequestId: options.pullRequestId,
522 | 				};
523 | 
524 | 				actionLogger.debug('Approving pull request:', params);
525 | 				const result =
526 | 					await atlassianPullRequestsController.approve(params);
527 | 				actionLogger.debug('Successfully approved pull request');
528 | 
529 | 				console.log(result.content);
530 | 			} catch (error) {
531 | 				actionLogger.error('Operation failed:', error);
532 | 				handleCliError(error);
533 | 			}
534 | 		});
535 | }
536 | 
537 | /**
538 |  * Register the command for requesting changes on a Bitbucket pull request
539 |  * @param program - The Commander program instance
540 |  */
541 | function registerRejectPullRequestCommand(program: Command): void {
542 | 	program
543 | 		.command('reject-pr')
544 | 		.description(
545 | 			'Request changes on a pull request in a Bitbucket repository.',
546 | 		)
547 | 		.option(
548 | 			'-w, --workspace-slug <slug>',
549 | 			'Workspace slug containing the repository (optional, uses default workspace if not provided). Example: "myteam"',
550 | 		)
551 | 		.requiredOption(
552 | 			'-r, --repo-slug <slug>',
553 | 			'Repository slug containing the pull request. Example: "project-api"',
554 | 		)
555 | 		.requiredOption(
556 | 			'-p, --pull-request-id <id>',
557 | 			'Pull request ID to request changes on. Example: 123',
558 | 			parseInt,
559 | 		)
560 | 		.action(async (options) => {
561 | 			const actionLogger = Logger.forContext(
562 | 				'cli/atlassian.pullrequests.cli.ts',
563 | 				'reject-pr',
564 | 			);
565 | 			try {
566 | 				actionLogger.debug('Processing command options:', options);
567 | 
568 | 				// Map CLI options to controller params
569 | 				const params = {
570 | 					workspaceSlug: options.workspaceSlug,
571 | 					repoSlug: options.repoSlug,
572 | 					pullRequestId: options.pullRequestId,
573 | 				};
574 | 
575 | 				actionLogger.debug(
576 | 					'Requesting changes on pull request:',
577 | 					params,
578 | 				);
579 | 				const result =
580 | 					await atlassianPullRequestsController.reject(params);
581 | 				actionLogger.debug(
582 | 					'Successfully requested changes on pull request',
583 | 				);
584 | 
585 | 				console.log(result.content);
586 | 			} catch (error) {
587 | 				actionLogger.error('Operation failed:', error);
588 | 				handleCliError(error);
589 | 			}
590 | 		});
591 | }
592 | 
593 | export default { register };
594 | 
```

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

```typescript
  1 | import { CliTestUtil } from '../utils/cli.test.util.js';
  2 | import { getAtlassianCredentials } from '../utils/transport.util.js';
  3 | import { config } from '../utils/config.util.js';
  4 | 
  5 | describe('Atlassian Pull Requests CLI Commands', () => {
  6 | 	// Load configuration and check for credentials before all tests
  7 | 	beforeAll(() => {
  8 | 		// Load configuration from all sources
  9 | 		config.load();
 10 | 
 11 | 		// Log warning if credentials aren't available
 12 | 		const credentials = getAtlassianCredentials();
 13 | 		if (!credentials) {
 14 | 			console.warn(
 15 | 				'Skipping Atlassian Pull Requests CLI tests: No credentials available',
 16 | 			);
 17 | 		}
 18 | 	});
 19 | 
 20 | 	// Helper function to skip tests when credentials are missing
 21 | 	const skipIfNoCredentials = () => {
 22 | 		const credentials = getAtlassianCredentials();
 23 | 		if (!credentials) {
 24 | 			return true;
 25 | 		}
 26 | 		return false;
 27 | 	};
 28 | 
 29 | 	// Helper to get workspace and repository for testing
 30 | 	async function getWorkspaceAndRepo(): Promise<{
 31 | 		workspace: string;
 32 | 		repository: string;
 33 | 	} | null> {
 34 | 		// First, get a list of workspaces
 35 | 		const workspacesResult = await CliTestUtil.runCommand([
 36 | 			'ls-workspaces',
 37 | 		]);
 38 | 
 39 | 		// Skip if no workspaces are available
 40 | 		if (
 41 | 			workspacesResult.stdout.includes('No Bitbucket workspaces found.')
 42 | 		) {
 43 | 			return null; // Skip silently for this helper function
 44 | 		}
 45 | 
 46 | 		// Extract a workspace slug from the output
 47 | 		const slugMatch = workspacesResult.stdout.match(
 48 | 			/\*\*Slug\*\*:\s+([^\n]+)/,
 49 | 		);
 50 | 		if (!slugMatch || !slugMatch[1]) {
 51 | 			return null; // Skip silently for this helper function
 52 | 		}
 53 | 
 54 | 		const workspaceSlug = slugMatch[1].trim();
 55 | 
 56 | 		// Get repositories for this workspace
 57 | 		const reposResult = await CliTestUtil.runCommand([
 58 | 			'ls-repos',
 59 | 			'--workspace-slug',
 60 | 			workspaceSlug,
 61 | 		]);
 62 | 
 63 | 		// Skip if no repositories are available
 64 | 		if (reposResult.stdout.includes('No repositories found')) {
 65 | 			return null; // Skip silently for this helper function
 66 | 		}
 67 | 
 68 | 		// Extract a repository slug from the output
 69 | 		const repoMatch = reposResult.stdout.match(/\*\*Name\*\*:\s+([^\n]+)/);
 70 | 		if (!repoMatch || !repoMatch[1]) {
 71 | 			return null; // Skip silently for this helper function
 72 | 		}
 73 | 
 74 | 		const repoSlug = repoMatch[1].trim();
 75 | 
 76 | 		return {
 77 | 			workspace: workspaceSlug,
 78 | 			repository: repoSlug,
 79 | 		};
 80 | 	}
 81 | 
 82 | 	describe('ls-prs command', () => {
 83 | 		it('should list pull requests for a repository', async () => {
 84 | 			if (skipIfNoCredentials()) {
 85 | 				return;
 86 | 			}
 87 | 
 88 | 			// Get a valid workspace and repository
 89 | 			const repoInfo = await getWorkspaceAndRepo();
 90 | 			if (!repoInfo) {
 91 | 				return; // Skip if no valid workspace/repo found
 92 | 			}
 93 | 
 94 | 			// Run the CLI command
 95 | 			const result = await CliTestUtil.runCommand([
 96 | 				'ls-prs',
 97 | 				'--workspace-slug',
 98 | 				repoInfo.workspace,
 99 | 				'--repo-slug',
100 | 				repoInfo.repository,
101 | 			]);
102 | 
103 | 			// Instead of expecting success, handle both success and failure
104 | 			if (result.exitCode !== 0) {
105 | 				console.warn(
106 | 					'Skipping test validation: Could not list pull requests',
107 | 				);
108 | 				return;
109 | 			}
110 | 
111 | 			// Verify the output format
112 | 			if (!result.stdout.includes('No pull requests found')) {
113 | 				// Validate expected Markdown structure
114 | 				CliTestUtil.validateOutputContains(result.stdout, [
115 | 					'# Bitbucket Pull Requests',
116 | 					'**ID**',
117 | 					'**State**',
118 | 					'**Author**',
119 | 				]);
120 | 
121 | 				// Validate Markdown formatting
122 | 				CliTestUtil.validateMarkdownOutput(result.stdout);
123 | 			}
124 | 		}, 30000); // Increased timeout for API calls
125 | 
126 | 		it('should support filtering by state', async () => {
127 | 			if (skipIfNoCredentials()) {
128 | 				return;
129 | 			}
130 | 
131 | 			// Get a valid workspace and repository
132 | 			const repoInfo = await getWorkspaceAndRepo();
133 | 			if (!repoInfo) {
134 | 				return; // Skip if no valid workspace/repo found
135 | 			}
136 | 
137 | 			// States to test
138 | 			const states = ['OPEN', 'MERGED', 'DECLINED'];
139 | 			let testsPassed = false;
140 | 
141 | 			for (const state of states) {
142 | 				// Run the CLI command with state filter
143 | 				const result = await CliTestUtil.runCommand([
144 | 					'ls-prs',
145 | 					'--workspace-slug',
146 | 					repoInfo.workspace,
147 | 					'--repo-slug',
148 | 					repoInfo.repository,
149 | 					'--state',
150 | 					state,
151 | 				]);
152 | 
153 | 				// If any command succeeds, consider the test as passed
154 | 				if (result.exitCode === 0) {
155 | 					testsPassed = true;
156 | 
157 | 					// Verify the output includes state if PRs are found
158 | 					if (!result.stdout.includes('No pull requests found')) {
159 | 						expect(result.stdout.toUpperCase()).toContain(state);
160 | 					}
161 | 				}
162 | 			}
163 | 
164 | 			// If all commands failed, log a warning and consider the test skipped
165 | 			if (!testsPassed) {
166 | 				console.warn(
167 | 					'Skipping test validation: Could not list pull requests with state filter',
168 | 				);
169 | 			}
170 | 		}, 45000); // Increased timeout for multiple API calls
171 | 
172 | 		it('should support pagination with --limit flag', async () => {
173 | 			if (skipIfNoCredentials()) {
174 | 				return;
175 | 			}
176 | 
177 | 			// Get a valid workspace and repository
178 | 			const repoInfo = await getWorkspaceAndRepo();
179 | 			if (!repoInfo) {
180 | 				return; // Skip if no valid workspace/repo found
181 | 			}
182 | 
183 | 			// Run the CLI command with limit
184 | 			const result = await CliTestUtil.runCommand([
185 | 				'ls-prs',
186 | 				'--workspace-slug',
187 | 				repoInfo.workspace,
188 | 				'--repo-slug',
189 | 				repoInfo.repository,
190 | 				'--limit',
191 | 				'1',
192 | 			]);
193 | 
194 | 			// Instead of expecting success, handle both success and failure
195 | 			if (result.exitCode !== 0) {
196 | 				console.warn(
197 | 					'Skipping test validation: Could not list pull requests with pagination',
198 | 				);
199 | 				return;
200 | 			}
201 | 
202 | 			// If there are multiple PRs, pagination section should be present
203 | 			if (
204 | 				!result.stdout.includes('No pull requests found') &&
205 | 				result.stdout.includes('items remaining')
206 | 			) {
207 | 				CliTestUtil.validateOutputContains(result.stdout, [
208 | 					'Pagination',
209 | 					'Next cursor:',
210 | 				]);
211 | 			}
212 | 		}, 30000);
213 | 	});
214 | 
215 | 	describe('get-pr command', () => {
216 | 		it('should retrieve details for a specific pull request', async () => {
217 | 			if (skipIfNoCredentials()) {
218 | 				return;
219 | 			}
220 | 
221 | 			// Get a valid workspace and repository
222 | 			const repoInfo = await getWorkspaceAndRepo();
223 | 			if (!repoInfo) {
224 | 				return; // Skip if no valid workspace/repo found
225 | 			}
226 | 
227 | 			// First, list pull requests to find a valid ID
228 | 			const listResult = await CliTestUtil.runCommand([
229 | 				'ls-prs',
230 | 				'--workspace-slug',
231 | 				repoInfo.workspace,
232 | 				'--repo-slug',
233 | 				repoInfo.repository,
234 | 			]);
235 | 
236 | 			// Skip if no pull requests are available
237 | 			if (listResult.stdout.includes('No pull requests found')) {
238 | 				return; // Skip silently - no pull requests available
239 | 			}
240 | 
241 | 			// Extract a pull request ID from the output
242 | 			const prMatch = listResult.stdout.match(/\*\*ID\*\*:\s+(\d+)/);
243 | 			if (!prMatch || !prMatch[1]) {
244 | 				console.warn(
245 | 					'Skipping test: Could not extract pull request ID',
246 | 				);
247 | 				return;
248 | 			}
249 | 
250 | 			const prId = prMatch[1].trim();
251 | 
252 | 			// Skip the full validation since we can't guarantee PRs exist
253 | 			// Just verify that the test can find a valid ID and run the command
254 | 			if (prId) {
255 | 				// Run the get-pr command
256 | 				const getResult = await CliTestUtil.runCommand([
257 | 					'get-pr',
258 | 					'--workspace-slug',
259 | 					repoInfo.workspace,
260 | 					'--repo-slug',
261 | 					repoInfo.repository,
262 | 					'--pr-id',
263 | 					prId,
264 | 				]);
265 | 
266 | 				// The test may pass or fail depending on if the PR exists
267 | 				// Just check that we get a result back
268 | 				expect(getResult).toBeDefined();
269 | 			} else {
270 | 				// Skip test if no PR ID found
271 | 				return; // Skip silently - no pull request ID available
272 | 			}
273 | 		}, 45000); // Increased timeout for multiple API calls
274 | 
275 | 		it('should handle missing required parameters', async () => {
276 | 			if (skipIfNoCredentials()) {
277 | 				return;
278 | 			}
279 | 
280 | 			// Test without workspace parameter (now optional)
281 | 			const missingWorkspace = await CliTestUtil.runCommand([
282 | 				'get-pr',
283 | 				'--repo-slug',
284 | 				'some-repo',
285 | 				'--pr-id',
286 | 				'1',
287 | 			]);
288 | 
289 | 			// Now that workspace is optional, we should get a different error
290 | 			// (repository not found), but not a missing parameter error
291 | 			expect(missingWorkspace.exitCode).not.toBe(0);
292 | 			expect(missingWorkspace.stderr).not.toContain('workspace-slug');
293 | 
294 | 			// Test without repository parameter
295 | 			const missingRepo = await CliTestUtil.runCommand([
296 | 				'get-pr',
297 | 				'--workspace-slug',
298 | 				'some-workspace',
299 | 				'--pr-id',
300 | 				'1',
301 | 			]);
302 | 
303 | 			// Should fail with non-zero exit code
304 | 			expect(missingRepo.exitCode).not.toBe(0);
305 | 			expect(missingRepo.stderr).toContain('required option');
306 | 
307 | 			// Test without pull-request parameter
308 | 			const missingPR = await CliTestUtil.runCommand([
309 | 				'get-pr',
310 | 				'--workspace-slug',
311 | 				'some-workspace',
312 | 				'--repo-slug',
313 | 				'some-repo',
314 | 			]);
315 | 
316 | 			// Should fail with non-zero exit code
317 | 			expect(missingPR.exitCode).not.toBe(0);
318 | 			expect(missingPR.stderr).toContain('required option');
319 | 		}, 15000);
320 | 	});
321 | 
322 | 	describe('ls-pr-comments command', () => {
323 | 		it('should list comments for a specific pull request', async () => {
324 | 			if (skipIfNoCredentials()) {
325 | 				return;
326 | 			}
327 | 
328 | 			// Get a valid workspace and repository
329 | 			const repoInfo = await getWorkspaceAndRepo();
330 | 			if (!repoInfo) {
331 | 				return; // Skip if no valid workspace/repo found
332 | 			}
333 | 
334 | 			// First, list pull requests to find a valid ID
335 | 			const listResult = await CliTestUtil.runCommand([
336 | 				'ls-prs',
337 | 				'--workspace-slug',
338 | 				repoInfo.workspace,
339 | 				'--repo-slug',
340 | 				repoInfo.repository,
341 | 			]);
342 | 
343 | 			// Skip if no pull requests are available
344 | 			if (listResult.stdout.includes('No pull requests found')) {
345 | 				return; // Skip silently - no pull requests available
346 | 			}
347 | 
348 | 			// Extract a pull request ID from the output
349 | 			const prMatch = listResult.stdout.match(/\*\*ID\*\*:\s+(\d+)/);
350 | 			if (!prMatch || !prMatch[1]) {
351 | 				console.warn(
352 | 					'Skipping test: Could not extract pull request ID',
353 | 				);
354 | 				return;
355 | 			}
356 | 
357 | 			const prId = prMatch[1].trim();
358 | 
359 | 			// Skip the full validation since we can't guarantee PRs exist
360 | 			// Just verify that the test can find a valid ID and run the command
361 | 			if (prId) {
362 | 				// Run the ls-pr-comments command
363 | 				const result = await CliTestUtil.runCommand([
364 | 					'ls-pr-comments',
365 | 					'--workspace-slug',
366 | 					repoInfo.workspace,
367 | 					'--repo-slug',
368 | 					repoInfo.repository,
369 | 					'--pr-id',
370 | 					prId,
371 | 				]);
372 | 
373 | 				// The test may pass or fail depending on if the PR exists
374 | 				// Just check that we get a result back
375 | 				expect(result).toBeDefined();
376 | 			} else {
377 | 				// Skip test if no PR ID found
378 | 				return; // Skip silently - no pull request ID available
379 | 			}
380 | 		}, 45000); // Increased timeout for multiple API calls
381 | 
382 | 		it('should handle missing required parameters', async () => {
383 | 			if (skipIfNoCredentials()) {
384 | 				return;
385 | 			}
386 | 
387 | 			// Test without workspace parameter (now optional)
388 | 			const missingWorkspace = await CliTestUtil.runCommand([
389 | 				'ls-pr-comments',
390 | 				'--repo-slug',
391 | 				'some-repo',
392 | 				'--pr-id',
393 | 				'1',
394 | 			]);
395 | 
396 | 			// Now that workspace is optional, we should get a different error
397 | 			// (repository not found), but not a missing parameter error
398 | 			expect(missingWorkspace.exitCode).not.toBe(0);
399 | 			expect(missingWorkspace.stderr).not.toContain('workspace-slug');
400 | 
401 | 			// Test without repository parameter
402 | 			const missingRepo = await CliTestUtil.runCommand([
403 | 				'ls-pr-comments',
404 | 				'--workspace-slug',
405 | 				'some-workspace',
406 | 				'--pr-id',
407 | 				'1',
408 | 			]);
409 | 
410 | 			// Should fail with non-zero exit code
411 | 			expect(missingRepo.exitCode).not.toBe(0);
412 | 			expect(missingRepo.stderr).toContain('required option');
413 | 
414 | 			// Test without pull-request parameter
415 | 			const missingPR = await CliTestUtil.runCommand([
416 | 				'ls-pr-comments',
417 | 				'--workspace-slug',
418 | 				'some-workspace',
419 | 				'--repo-slug',
420 | 				'some-repo',
421 | 			]);
422 | 
423 | 			// Should fail with non-zero exit code
424 | 			expect(missingPR.exitCode).not.toBe(0);
425 | 			expect(missingPR.stderr).toContain('required option');
426 | 		}, 15000);
427 | 	});
428 | 
429 | 	describe('ls-pr-comments with pagination', () => {
430 | 		it('should list comments for a specific pull request with pagination', async () => {
431 | 			if (skipIfNoCredentials()) {
432 | 				return;
433 | 			}
434 | 
435 | 			// Get a valid workspace and repository
436 | 			const repoInfo = await getWorkspaceAndRepo();
437 | 			if (!repoInfo) {
438 | 				return; // Skip if no valid workspace/repo found
439 | 			}
440 | 
441 | 			// First, list pull requests to find a valid ID
442 | 			const listResult = await CliTestUtil.runCommand([
443 | 				'ls-prs',
444 | 				'--workspace-slug',
445 | 				repoInfo.workspace,
446 | 				'--repo-slug',
447 | 				repoInfo.repository,
448 | 			]);
449 | 
450 | 			// Skip if no pull requests are available
451 | 			if (listResult.stdout.includes('No pull requests found')) {
452 | 				return; // Skip silently - no pull requests available
453 | 			}
454 | 
455 | 			// Extract a pull request ID from the output
456 | 			const prMatch = listResult.stdout.match(/\*\*ID\*\*:\s+(\d+)/);
457 | 			if (!prMatch || !prMatch[1]) {
458 | 				console.warn(
459 | 					'Skipping test: Could not extract pull request ID',
460 | 				);
461 | 				return;
462 | 			}
463 | 
464 | 			const prId = prMatch[1].trim();
465 | 
466 | 			// Skip the full validation since we can't guarantee PRs exist
467 | 			// Just verify that the test can find a valid ID and run the command
468 | 			if (prId) {
469 | 				// Run with pagination limit
470 | 				const limitResult = await CliTestUtil.runCommand([
471 | 					'ls-pr-comments',
472 | 					'--workspace-slug',
473 | 					repoInfo.workspace,
474 | 					'--repo-slug',
475 | 					repoInfo.repository,
476 | 					'--pr-id',
477 | 					prId,
478 | 					'--limit',
479 | 					'1',
480 | 				]);
481 | 
482 | 				// The test may pass or fail depending on if the PR exists
483 | 				// Just check that we get a result back
484 | 				expect(limitResult).toBeDefined();
485 | 			} else {
486 | 				// Skip test if no PR ID found
487 | 				return; // Skip silently - no pull request ID available
488 | 			}
489 | 		}, 45000); // Increased timeout for multiple API calls
490 | 	});
491 | 
492 | 	describe('add-pr-comment command', () => {
493 | 		it('should display help information', async () => {
494 | 			const result = await CliTestUtil.runCommand([
495 | 				'add-pr-comment',
496 | 				'--help',
497 | 			]);
498 | 			expect(result.exitCode).toBe(0);
499 | 			expect(result.stdout).toContain(
500 | 				'Add a comment to a Bitbucket pull request.',
501 | 			);
502 | 			expect(result.stdout).toContain('--workspace-slug');
503 | 			expect(result.stdout).toContain('--repo-slug');
504 | 			expect(result.stdout).toContain('--pr-id');
505 | 			expect(result.stdout).toContain('--content');
506 | 			expect(result.stdout).toContain('--path');
507 | 			expect(result.stdout).toContain('--line');
508 | 			expect(result.stdout).toContain('--parent-id');
509 | 		});
510 | 
511 | 		it('should require repo-slug parameter', async () => {
512 | 			const result = await CliTestUtil.runCommand([
513 | 				'add-pr-comment',
514 | 				'--workspace-slug',
515 | 				'codapayments',
516 | 			]);
517 | 			expect(result.exitCode).not.toBe(0);
518 | 			expect(result.stderr).toContain('required option');
519 | 			expect(result.stderr).toContain('repo-slug');
520 | 		});
521 | 
522 | 		it('should use default workspace when workspace is not provided', async () => {
523 | 			const result = await CliTestUtil.runCommand(['add-pr-comment']);
524 | 			expect(result.exitCode).not.toBe(0);
525 | 			expect(result.stderr).toContain('required option');
526 | 			// Should require repo-slug but not workspace-slug
527 | 			expect(result.stderr).toContain('repo-slug');
528 | 			expect(result.stderr).not.toContain('workspace-slug');
529 | 		});
530 | 
531 | 		it('should require pr-id parameter', async () => {
532 | 			const result = await CliTestUtil.runCommand([
533 | 				'add-pr-comment',
534 | 				'--workspace-slug',
535 | 				'codapayments',
536 | 				'--repo-slug',
537 | 				'repo-1',
538 | 			]);
539 | 			expect(result.exitCode).not.toBe(0);
540 | 			expect(result.stderr).toContain('required option');
541 | 			expect(result.stderr).toContain('pr-id');
542 | 		});
543 | 
544 | 		it('should require content parameter', async () => {
545 | 			const result = await CliTestUtil.runCommand([
546 | 				'add-pr-comment',
547 | 				'--workspace-slug',
548 | 				'codapayments',
549 | 				'--repo-slug',
550 | 				'repo-1',
551 | 				'--pr-id',
552 | 				'1',
553 | 			]);
554 | 			expect(result.exitCode).not.toBe(0);
555 | 			expect(result.stderr).toContain('required option');
556 | 			expect(result.stderr).toContain('content');
557 | 		});
558 | 
559 | 		it('should detect incomplete inline comment parameters', async () => {
560 | 			const result = await CliTestUtil.runCommand([
561 | 				'add-pr-comment',
562 | 				'--workspace-slug',
563 | 				'codapayments',
564 | 				'--repo-slug',
565 | 				'repo-1',
566 | 				'--pr-id',
567 | 				'1',
568 | 				'--content',
569 | 				'Test',
570 | 				'--path',
571 | 				'README.md',
572 | 			]);
573 | 			expect(result.exitCode).not.toBe(0);
574 | 			expect(result.stderr).toContain(
575 | 				'Both -f/--path and -L/--line are required for inline comments',
576 | 			);
577 | 		});
578 | 
579 | 		it('should accept valid parentId parameter for comment replies', async () => {
580 | 			const result = await CliTestUtil.runCommand([
581 | 				'add-pr-comment',
582 | 				'--workspace-slug',
583 | 				'codapayments',
584 | 				'--repo-slug',
585 | 				'repo-1',
586 | 				'--pr-id',
587 | 				'1',
588 | 				'--content',
589 | 				'This is a reply',
590 | 				'--parent-id',
591 | 				'123',
592 | 			]);
593 | 			// Should fail due to invalid repo/PR, but not due to parentId parameter validation
594 | 			expect(result.exitCode).not.toBe(0);
595 | 			// Should NOT contain parameter validation errors for parentId
596 | 			expect(result.stderr).not.toContain('parent-id');
597 | 		});
598 | 
599 | 		// Note: API call test has been removed to avoid creating comments on real PRs during tests
600 | 	});
601 | 
602 | 	describe('add-pr command', () => {
603 | 		it('should display help information', async () => {
604 | 			const result = await CliTestUtil.runCommand(['add-pr', '--help']);
605 | 			expect(result.exitCode).toBe(0);
606 | 			expect(result.stdout).toContain(
607 | 				'Add a new pull request in a Bitbucket repository.',
608 | 			);
609 | 			expect(result.stdout).toContain('--workspace-slug');
610 | 			expect(result.stdout).toContain('--repo-slug');
611 | 			expect(result.stdout).toContain('--title');
612 | 			expect(result.stdout).toContain('--source-branch');
613 | 			expect(result.stdout).toContain('--destination-branch');
614 | 			expect(result.stdout).toContain('--description');
615 | 			expect(result.stdout).toContain('--close-source-branch');
616 | 		});
617 | 
618 | 		it('should require repo-slug parameter', async () => {
619 | 			const result = await CliTestUtil.runCommand([
620 | 				'add-pr',
621 | 				'--workspace-slug',
622 | 				'test-ws',
623 | 			]);
624 | 			expect(result.exitCode).not.toBe(0);
625 | 			expect(result.stderr).toContain('required option');
626 | 			expect(result.stderr).toContain('repo-slug');
627 | 		});
628 | 
629 | 		it('should use default workspace when workspace is not provided', async () => {
630 | 			const result = await CliTestUtil.runCommand(['add-pr']);
631 | 			expect(result.exitCode).not.toBe(0);
632 | 			expect(result.stderr).toContain('required option');
633 | 			// Should require repo-slug but not workspace-slug
634 | 			expect(result.stderr).toContain('repo-slug');
635 | 			expect(result.stderr).not.toContain('workspace-slug');
636 | 		});
637 | 
638 | 		it('should require title parameter', async () => {
639 | 			const result = await CliTestUtil.runCommand([
640 | 				'add-pr',
641 | 				'--workspace-slug',
642 | 				'test-ws',
643 | 				'--repo-slug',
644 | 				'test-repo',
645 | 			]);
646 | 			expect(result.exitCode).not.toBe(0);
647 | 			expect(result.stderr).toContain('required option');
648 | 			expect(result.stderr).toContain('title');
649 | 		});
650 | 
651 | 		it('should require source-branch parameter', async () => {
652 | 			const result = await CliTestUtil.runCommand([
653 | 				'add-pr',
654 | 				'--workspace-slug',
655 | 				'test-ws',
656 | 				'--repo-slug',
657 | 				'test-repo',
658 | 				'--title',
659 | 				'Test PR',
660 | 			]);
661 | 			expect(result.exitCode).not.toBe(0);
662 | 			expect(result.stderr).toContain('required option');
663 | 			expect(result.stderr).toContain('source-branch');
664 | 		});
665 | 
666 | 		// Note: API call test has been removed to avoid creating PRs on real repos during tests
667 | 	});
668 | });
669 | 
```

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

```typescript
  1 | import { McpError, ErrorType } from '../utils/error.util.js';
  2 | import atlassianPullRequestsController from './atlassian.pullrequests.controller.js';
  3 | import { getAtlassianCredentials } from '../utils/transport.util.js';
  4 | import atlassianPullRequestsService from '../services/vendor.atlassian.pullrequests.service.js';
  5 | import atlassianWorkspacesService from '../services/vendor.atlassian.workspaces.service.js';
  6 | import atlassianRepositoriesService from '../services/vendor.atlassian.repositories.service.js';
  7 | 
  8 | describe('Atlassian Pull Requests Controller', () => {
  9 | 	const skipIfNoCredentials = () => !getAtlassianCredentials();
 10 | 
 11 | 	async function getRepositoryInfo(): Promise<{
 12 | 		workspaceSlug: string;
 13 | 		repoSlug: string;
 14 | 	} | null> {
 15 | 		// Skip if no credentials
 16 | 		if (skipIfNoCredentials()) return null;
 17 | 
 18 | 		try {
 19 | 			// Try to get workspaces
 20 | 			const workspacesResponse = await atlassianWorkspacesService.list({
 21 | 				pagelen: 1,
 22 | 			});
 23 | 
 24 | 			if (
 25 | 				!workspacesResponse.values ||
 26 | 				workspacesResponse.values.length === 0
 27 | 			) {
 28 | 				console.warn('No workspaces found for test account');
 29 | 				return null;
 30 | 			}
 31 | 
 32 | 			const workspace = workspacesResponse.values[0];
 33 | 			// The workspace slug might be in different locations depending on the response structure
 34 | 			// Try to access it safely from possible locations
 35 | 			const workspaceSlug =
 36 | 				workspace.workspace?.slug ||
 37 | 				(workspace as any).slug ||
 38 | 				(workspace as any).name ||
 39 | 				null;
 40 | 
 41 | 			if (!workspaceSlug) {
 42 | 				console.warn(
 43 | 					'Could not determine workspace slug from response',
 44 | 				);
 45 | 				return null;
 46 | 			}
 47 | 
 48 | 			// Get repositories for this workspace
 49 | 			const reposResponse = await atlassianRepositoriesService.list({
 50 | 				workspace: workspaceSlug,
 51 | 				pagelen: 1,
 52 | 			});
 53 | 
 54 | 			if (!reposResponse.values || reposResponse.values.length === 0) {
 55 | 				console.warn(
 56 | 					`No repositories found in workspace ${workspaceSlug}`,
 57 | 				);
 58 | 				return null;
 59 | 			}
 60 | 
 61 | 			const repo = reposResponse.values[0];
 62 | 			// The repo slug might be in different locations or named differently
 63 | 			const repoSlug =
 64 | 				(repo as any).slug ||
 65 | 				(repo as any).name ||
 66 | 				(repo.full_name?.split('/').pop() as string) ||
 67 | 				null;
 68 | 
 69 | 			if (!repoSlug) {
 70 | 				console.warn(
 71 | 					'Could not determine repository slug from response',
 72 | 				);
 73 | 				return null;
 74 | 			}
 75 | 
 76 | 			return {
 77 | 				workspaceSlug,
 78 | 				repoSlug,
 79 | 			};
 80 | 		} catch (error) {
 81 | 			console.warn('Failed to get repository info for tests:', error);
 82 | 			return null;
 83 | 		}
 84 | 	}
 85 | 
 86 | 	async function getFirstPullRequestId(): Promise<{
 87 | 		workspaceSlug: string;
 88 | 		repoSlug: string;
 89 | 		prId: string;
 90 | 	} | null> {
 91 | 		const repoInfo = await getRepositoryInfo();
 92 | 		if (!repoInfo) return null;
 93 | 
 94 | 		try {
 95 | 			// List PRs
 96 | 			const prsResponse = await atlassianPullRequestsService.list({
 97 | 				workspace: repoInfo.workspaceSlug,
 98 | 				repo_slug: repoInfo.repoSlug,
 99 | 				pagelen: 1,
100 | 			});
101 | 
102 | 			if (!prsResponse.values || prsResponse.values.length === 0) {
103 | 				console.warn(
104 | 					`No pull requests found in ${repoInfo.workspaceSlug}/${repoInfo.repoSlug}`,
105 | 				);
106 | 				return null;
107 | 			}
108 | 
109 | 			// Return the first PR's info
110 | 			return {
111 | 				workspaceSlug: repoInfo.workspaceSlug,
112 | 				repoSlug: repoInfo.repoSlug,
113 | 				prId: prsResponse.values[0].id.toString(),
114 | 			};
115 | 		} catch (error) {
116 | 			console.warn('Failed to get pull request ID for tests:', error);
117 | 			return null;
118 | 		}
119 | 	}
120 | 
121 | 	async function getPullRequestInfo(): Promise<{
122 | 		workspaceSlug: string;
123 | 		repoSlug: string;
124 | 		prId: string;
125 | 	} | null> {
126 | 		// Attempt to get a first PR ID
127 | 		return await getFirstPullRequestId();
128 | 	}
129 | 
130 | 	// List tests
131 | 	describe('list', () => {
132 | 		it('should list pull requests for a repository', async () => {
133 | 			if (skipIfNoCredentials()) return;
134 | 
135 | 			const repoInfo = await getRepositoryInfo();
136 | 			if (!repoInfo) {
137 | 				console.warn('Skipping test: No repository info found.');
138 | 				return;
139 | 			}
140 | 
141 | 			const result = await atlassianPullRequestsController.list({
142 | 				workspaceSlug: repoInfo.workspaceSlug,
143 | 				repoSlug: repoInfo.repoSlug,
144 | 			});
145 | 
146 | 			// Verify the response structure
147 | 			expect(result).toHaveProperty('content');
148 | 			expect(typeof result.content).toBe('string');
149 | 
150 | 			// Check for expected format based on the formatter output
151 | 			expect(result.content).toMatch(/^# Pull Requests/);
152 | 		}, 30000);
153 | 
154 | 		it('should handle query parameters correctly', async () => {
155 | 			if (skipIfNoCredentials()) return;
156 | 
157 | 			const repoInfo = await getRepositoryInfo();
158 | 			if (!repoInfo) {
159 | 				console.warn('Skipping test: No repository info found.');
160 | 				return;
161 | 			}
162 | 
163 | 			// A query term that should not match any PRs in any normal repo
164 | 			const uniqueQuery = 'z9x8c7vb6nm5';
165 | 
166 | 			const result = await atlassianPullRequestsController.list({
167 | 				workspaceSlug: repoInfo.workspaceSlug,
168 | 				repoSlug: repoInfo.repoSlug,
169 | 				query: uniqueQuery,
170 | 			});
171 | 
172 | 			// Even with no matches, we expect a formatted empty list
173 | 			expect(result).toHaveProperty('content');
174 | 			expect(result.content).toMatch(/^# Pull Requests/);
175 | 			expect(result.content).toContain('No pull requests found');
176 | 		}, 30000);
177 | 
178 | 		it('should handle pagination options (limit)', async () => {
179 | 			if (skipIfNoCredentials()) return;
180 | 
181 | 			const repoInfo = await getRepositoryInfo();
182 | 			if (!repoInfo) {
183 | 				console.warn('Skipping test: No repository info found.');
184 | 				return;
185 | 			}
186 | 
187 | 			// Fetch with a small limit
188 | 			const result = await atlassianPullRequestsController.list({
189 | 				workspaceSlug: repoInfo.workspaceSlug,
190 | 				repoSlug: repoInfo.repoSlug,
191 | 				limit: 1,
192 | 			});
193 | 
194 | 			// Verify the response structure
195 | 			expect(result).toHaveProperty('content');
196 | 
197 | 			// Check if the result contains only one PR or none
198 | 			// First check if there are no PRs
199 | 			if (result.content.includes('No pull requests found')) {
200 | 				console.warn('No pull requests found for pagination test.');
201 | 				return;
202 | 			}
203 | 
204 | 			// Extract count
205 | 			const countMatch = result.content.match(/\*Showing (\d+) item/);
206 | 			if (countMatch && countMatch[1]) {
207 | 				const count = parseInt(countMatch[1], 10);
208 | 				expect(count).toBeLessThanOrEqual(1);
209 | 			}
210 | 		}, 30000);
211 | 
212 | 		it('should handle authorization errors gracefully', async () => {
213 | 			if (skipIfNoCredentials()) return;
214 | 
215 | 			const repoInfo = await getRepositoryInfo();
216 | 			if (!repoInfo) {
217 | 				console.warn('Skipping test: No repository info found.');
218 | 				return;
219 | 			}
220 | 
221 | 			// Create mocked credentials error by using intentionally invalid credentials
222 | 			jest.spyOn(
223 | 				atlassianPullRequestsService,
224 | 				'list',
225 | 			).mockRejectedValueOnce(
226 | 				new McpError('Unauthorized', ErrorType.AUTH_INVALID, 401),
227 | 			);
228 | 
229 | 			// Expect to throw a standardized error
230 | 			await expect(
231 | 				atlassianPullRequestsController.list({
232 | 					workspaceSlug: repoInfo.workspaceSlug,
233 | 					repoSlug: repoInfo.repoSlug,
234 | 				}),
235 | 			).rejects.toThrow(McpError);
236 | 
237 | 			// Verify it was called
238 | 			expect(atlassianPullRequestsService.list).toHaveBeenCalled();
239 | 
240 | 			// Cleanup mock
241 | 			jest.restoreAllMocks();
242 | 		}, 30000);
243 | 
244 | 		it('should require workspace and repository slugs', async () => {
245 | 			if (skipIfNoCredentials()) return;
246 | 
247 | 			// Missing both slugs
248 | 			await expect(
249 | 				atlassianPullRequestsController.list({} as any),
250 | 			).rejects.toThrow(
251 | 				/workspace slug and repository slug are required/i,
252 | 			);
253 | 
254 | 			// Missing repo slug
255 | 			await expect(
256 | 				atlassianPullRequestsController.list({
257 | 					workspaceSlug: 'some-workspace',
258 | 				} as any),
259 | 			).rejects.toThrow(
260 | 				/workspace slug and repository slug are required/i,
261 | 			);
262 | 
263 | 			// Missing workspace slug but should try to use default
264 | 			// This will fail with a different error if no default workspace can be found
265 | 			try {
266 | 				await atlassianPullRequestsController.list({
267 | 					repoSlug: 'some-repo',
268 | 				} as any);
269 | 			} catch (error) {
270 | 				expect(error).toBeInstanceOf(Error);
271 | 				// Either it will fail with "could not determine default" or
272 | 				// it may succeed with a default workspace but fail with a different error
273 | 			}
274 | 		}, 10000);
275 | 
276 | 		it('should apply default parameters correctly', async () => {
277 | 			if (skipIfNoCredentials()) return;
278 | 
279 | 			const repoInfo = await getRepositoryInfo();
280 | 			if (!repoInfo) {
281 | 				console.warn('Skipping test: No repository info found.');
282 | 				return;
283 | 			}
284 | 
285 | 			const serviceSpy = jest.spyOn(atlassianPullRequestsService, 'list');
286 | 
287 | 			try {
288 | 				await atlassianPullRequestsController.list({
289 | 					workspaceSlug: repoInfo.workspaceSlug,
290 | 					repoSlug: repoInfo.repoSlug,
291 | 					// No limit provided, should use default
292 | 				});
293 | 
294 | 				// Check that the service was called with the default limit
295 | 				const calls = serviceSpy.mock.calls;
296 | 				expect(calls.length).toBeGreaterThan(0);
297 | 				const lastCall = calls[calls.length - 1];
298 | 				expect(lastCall[0]).toHaveProperty('pagelen');
299 | 				expect(lastCall[0].pagelen).toBeGreaterThan(0); // Should have some positive default value
300 | 			} finally {
301 | 				serviceSpy.mockRestore();
302 | 			}
303 | 		}, 30000);
304 | 
305 | 		it('should format pagination information correctly', async () => {
306 | 			if (skipIfNoCredentials()) return;
307 | 
308 | 			const repoInfo = await getRepositoryInfo();
309 | 			if (!repoInfo) {
310 | 				console.warn('Skipping test: No repository info found.');
311 | 				return;
312 | 			}
313 | 
314 | 			// Mock a response with pagination
315 | 			const mockResponse = {
316 | 				values: [
317 | 					{
318 | 						id: 1,
319 | 						title: 'Test PR',
320 | 						state: 'OPEN',
321 | 						created_on: new Date().toISOString(),
322 | 						updated_on: new Date().toISOString(),
323 | 						author: {
324 | 							display_name: 'Test User',
325 | 						},
326 | 						source: {
327 | 							branch: { name: 'feature-branch' },
328 | 						},
329 | 						destination: {
330 | 							branch: { name: 'main' },
331 | 						},
332 | 						links: {},
333 | 					},
334 | 				],
335 | 				page: 1,
336 | 				size: 1,
337 | 				pagelen: 1,
338 | 				next: 'https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests?page=2',
339 | 			};
340 | 
341 | 			jest.spyOn(
342 | 				atlassianPullRequestsService,
343 | 				'list',
344 | 			).mockResolvedValueOnce(mockResponse as any);
345 | 
346 | 			const result = await atlassianPullRequestsController.list({
347 | 				workspaceSlug: repoInfo.workspaceSlug,
348 | 				repoSlug: repoInfo.repoSlug,
349 | 				limit: 1,
350 | 			});
351 | 
352 | 			// Verify pagination info is included
353 | 			expect(result.content).toContain('*Showing 1 item');
354 | 			expect(result.content).toContain('More results are available');
355 | 			expect(result.content).toContain('Next cursor: `2`');
356 | 
357 | 			// Cleanup mock
358 | 			jest.restoreAllMocks();
359 | 		}, 10000);
360 | 	});
361 | 
362 | 	// Get PR tests
363 | 	describe('get', () => {
364 | 		it('should retrieve detailed pull request information', async () => {
365 | 			if (skipIfNoCredentials()) return;
366 | 
367 | 			const prInfo = await getPullRequestInfo();
368 | 			if (!prInfo) {
369 | 				console.warn('Skipping test: No pull request info found.');
370 | 				return;
371 | 			}
372 | 
373 | 			const result = await atlassianPullRequestsController.get({
374 | 				workspaceSlug: prInfo.workspaceSlug,
375 | 				repoSlug: prInfo.repoSlug,
376 | 				prId: prInfo.prId,
377 | 				includeFullDiff: false,
378 | 				includeComments: false,
379 | 			});
380 | 
381 | 			// Verify the response structure
382 | 			expect(result).toHaveProperty('content');
383 | 			expect(typeof result.content).toBe('string');
384 | 
385 | 			// Check for expected format based on the formatter output
386 | 			expect(result.content).toMatch(/^# Pull Request/);
387 | 			expect(result.content).toContain('**ID**:');
388 | 			expect(result.content).toContain('**Title**:');
389 | 			expect(result.content).toContain('**State**:');
390 | 			expect(result.content).toContain('**Author**:');
391 | 			expect(result.content).toContain('**Created**:');
392 | 		}, 30000);
393 | 
394 | 		it('should handle errors for non-existent pull requests', async () => {
395 | 			if (skipIfNoCredentials()) return;
396 | 
397 | 			const repoInfo = await getRepositoryInfo();
398 | 			if (!repoInfo) {
399 | 				console.warn('Skipping test: No repository info found.');
400 | 				return;
401 | 			}
402 | 
403 | 			// Use a PR ID that is very unlikely to exist
404 | 			const nonExistentPrId = '999999999';
405 | 
406 | 			await expect(
407 | 				atlassianPullRequestsController.get({
408 | 					workspaceSlug: repoInfo.workspaceSlug,
409 | 					repoSlug: repoInfo.repoSlug,
410 | 					prId: nonExistentPrId,
411 | 					includeFullDiff: false,
412 | 					includeComments: false,
413 | 				}),
414 | 			).rejects.toThrow(McpError);
415 | 
416 | 			// Test that the error has the right status code and type
417 | 			try {
418 | 				await atlassianPullRequestsController.get({
419 | 					workspaceSlug: repoInfo.workspaceSlug,
420 | 					repoSlug: repoInfo.repoSlug,
421 | 					prId: nonExistentPrId,
422 | 					includeFullDiff: false,
423 | 					includeComments: false,
424 | 				});
425 | 			} catch (error) {
426 | 				expect(error).toBeInstanceOf(McpError);
427 | 				expect((error as McpError).statusCode).toBe(404);
428 | 				expect((error as McpError).message).toContain('not found');
429 | 			}
430 | 		}, 30000);
431 | 
432 | 		it('should require all necessary parameters', async () => {
433 | 			if (skipIfNoCredentials()) return;
434 | 
435 | 			// No parameters should throw
436 | 			await expect(
437 | 				atlassianPullRequestsController.get({} as any),
438 | 			).rejects.toThrow();
439 | 
440 | 			// Missing PR ID should throw
441 | 			const repoInfo = await getRepositoryInfo();
442 | 			if (repoInfo) {
443 | 				await expect(
444 | 					atlassianPullRequestsController.get({
445 | 						workspaceSlug: repoInfo.workspaceSlug,
446 | 						repoSlug: repoInfo.repoSlug,
447 | 						// prId: missing
448 | 					} as any),
449 | 				).rejects.toThrow();
450 | 			}
451 | 		}, 10000);
452 | 	});
453 | 
454 | 	// List comments tests
455 | 	describe('listComments', () => {
456 | 		it('should return formatted pull request comments in Markdown', async () => {
457 | 			if (skipIfNoCredentials()) return;
458 | 
459 | 			const prInfo = await getFirstPullRequestId();
460 | 			if (!prInfo) {
461 | 				console.warn('Skipping test: No pull request ID found.');
462 | 				return;
463 | 			}
464 | 
465 | 			const result = await atlassianPullRequestsController.listComments({
466 | 				workspaceSlug: prInfo.workspaceSlug,
467 | 				repoSlug: prInfo.repoSlug,
468 | 				prId: prInfo.prId,
469 | 			});
470 | 
471 | 			// Verify the response structure
472 | 			expect(result).toHaveProperty('content');
473 | 			expect(typeof result.content).toBe('string');
474 | 
475 | 			// Basic Markdown content checks
476 | 			if (result.content !== 'No comments found on this pull request.') {
477 | 				expect(result.content).toMatch(/^# Comments on Pull Request/m);
478 | 				expect(result.content).toContain('**Author**:');
479 | 				expect(result.content).toContain('**Updated**:');
480 | 				expect(result.content).toMatch(
481 | 					/---\s*\*Showing \d+ items?\.\*/,
482 | 				);
483 | 			}
484 | 		}, 30000);
485 | 
486 | 		it('should handle pagination options (limit/cursor)', async () => {
487 | 			if (skipIfNoCredentials()) return;
488 | 
489 | 			const prInfo = await getFirstPullRequestId();
490 | 			if (!prInfo) {
491 | 				console.warn('Skipping test: No pull request ID found.');
492 | 				return;
493 | 			}
494 | 
495 | 			// Fetch first page with limit 1
496 | 			const result1 = await atlassianPullRequestsController.listComments({
497 | 				workspaceSlug: prInfo.workspaceSlug,
498 | 				repoSlug: prInfo.repoSlug,
499 | 				prId: prInfo.prId,
500 | 				limit: 1,
501 | 			});
502 | 
503 | 			// Extract pagination info from content
504 | 			const countMatch1 = result1.content.match(
505 | 				/\*Showing (\d+) items?\.\*/,
506 | 			);
507 | 			const count1 = countMatch1 ? parseInt(countMatch1[1], 10) : 0;
508 | 			expect(count1).toBeLessThanOrEqual(1);
509 | 
510 | 			// Extract cursor from content
511 | 			const cursorMatch1 = result1.content.match(
512 | 				/\*Next cursor: `([^`]+)`\*/,
513 | 			);
514 | 			const nextCursor = cursorMatch1 ? cursorMatch1[1] : null;
515 | 
516 | 			// Check if pagination indicates more results
517 | 			const hasMoreResults = result1.content.includes(
518 | 				'More results are available.',
519 | 			);
520 | 
521 | 			// If there's a next page, fetch it
522 | 			if (hasMoreResults && nextCursor) {
523 | 				const result2 =
524 | 					await atlassianPullRequestsController.listComments({
525 | 						workspaceSlug: prInfo.workspaceSlug,
526 | 						repoSlug: prInfo.repoSlug,
527 | 						prId: prInfo.prId,
528 | 						limit: 1,
529 | 						cursor: nextCursor,
530 | 					});
531 | 
532 | 				// Extract count from result2
533 | 				const countMatch2 = result2.content.match(
534 | 					/\*Showing (\d+) items?\.\*/,
535 | 				);
536 | 				const count2 = countMatch2 ? parseInt(countMatch2[1], 10) : 0;
537 | 				expect(count2).toBeLessThanOrEqual(1);
538 | 
539 | 				// Ensure content is different (or handle case where only 1 comment exists)
540 | 				if (
541 | 					result1.content !==
542 | 						'No comments found on this pull request.' &&
543 | 					result2.content !==
544 | 						'No comments found on this pull request.' &&
545 | 					count1 > 0 &&
546 | 					count2 > 0
547 | 				) {
548 | 					// Only compare if we actually have multiple comments
549 | 					expect(result1.content).not.toEqual(result2.content);
550 | 				}
551 | 			} else {
552 | 				console.warn(
553 | 					'Skipping cursor part of pagination test: Only one page of comments found or no comments available.',
554 | 				);
555 | 			}
556 | 		}, 30000);
557 | 
558 | 		it('should handle empty result scenario', async () => {
559 | 			if (skipIfNoCredentials()) return;
560 | 
561 | 			// First get a PR without comments, or use a non-existent but valid format PR ID
562 | 			const repoInfo = await getRepositoryInfo();
563 | 			if (!repoInfo) {
564 | 				console.warn('Skipping test: No repository info found.');
565 | 				return;
566 | 			}
567 | 
568 | 			// Try to find a PR without comments
569 | 			try {
570 | 				// First check if we can access the repository
571 | 				const repoResult = await atlassianPullRequestsController.list({
572 | 					workspaceSlug: repoInfo.workspaceSlug,
573 | 					repoSlug: repoInfo.repoSlug,
574 | 					limit: 1,
575 | 				});
576 | 
577 | 				// Extract PR ID from the content if we have any PRs
578 | 				const prIdMatch =
579 | 					repoResult.content.match(/\*\*ID\*\*:\s+(\d+)/);
580 | 				if (!prIdMatch || !prIdMatch[1]) {
581 | 					console.warn(
582 | 						'Skipping empty result test: No pull requests available.',
583 | 					);
584 | 					return;
585 | 				}
586 | 
587 | 				const prId = prIdMatch[1];
588 | 
589 | 				// Get comments for this PR
590 | 				const commentsResult =
591 | 					await atlassianPullRequestsController.listComments({
592 | 						workspaceSlug: repoInfo.workspaceSlug,
593 | 						repoSlug: repoInfo.repoSlug,
594 | 						prId,
595 | 					});
596 | 
597 | 				// Check if there are no comments
598 | 				if (
599 | 					commentsResult.content ===
600 | 					'No comments found on this pull request.'
601 | 				) {
602 | 					// Verify the expected message for empty results
603 | 					expect(commentsResult.content).toBe(
604 | 						'No comments found on this pull request.',
605 | 					);
606 | 				} else {
607 | 					console.warn(
608 | 						'Skipping empty result test: All PRs have comments.',
609 | 					);
610 | 				}
611 | 			} catch (error) {
612 | 				console.warn('Error during empty result test:', error);
613 | 			}
614 | 		}, 30000);
615 | 
616 | 		it('should throw an McpError for a non-existent pull request ID', async () => {
617 | 			if (skipIfNoCredentials()) return;
618 | 
619 | 			const repoInfo = await getRepositoryInfo();
620 | 			if (!repoInfo) {
621 | 				console.warn('Skipping test: No repository info found.');
622 | 				return;
623 | 			}
624 | 
625 | 			const nonExistentId = '999999999'; // Very high number unlikely to exist
626 | 
627 | 			// Expect the controller call to reject with an McpError
628 | 			await expect(
629 | 				atlassianPullRequestsController.listComments({
630 | 					workspaceSlug: repoInfo.workspaceSlug,
631 | 					repoSlug: repoInfo.repoSlug,
632 | 					prId: nonExistentId,
633 | 				}),
634 | 			).rejects.toThrow(McpError);
635 | 
636 | 			// Check the status code via the error handler's behavior
637 | 			try {
638 | 				await atlassianPullRequestsController.listComments({
639 | 					workspaceSlug: repoInfo.workspaceSlug,
640 | 					repoSlug: repoInfo.repoSlug,
641 | 					prId: nonExistentId,
642 | 				});
643 | 			} catch (e) {
644 | 				expect(e).toBeInstanceOf(McpError);
645 | 				expect((e as McpError).statusCode).toBe(404); // Expecting Not Found
646 | 				expect((e as McpError).message).toContain('not found');
647 | 			}
648 | 		}, 30000);
649 | 
650 | 		it('should require all necessary parameters', async () => {
651 | 			if (skipIfNoCredentials()) return;
652 | 
653 | 			// No parameters
654 | 			await expect(
655 | 				atlassianPullRequestsController.listComments({} as any),
656 | 			).rejects.toThrow();
657 | 
658 | 			// Missing PR ID
659 | 			const repoInfo = await getRepositoryInfo();
660 | 			if (repoInfo) {
661 | 				await expect(
662 | 					atlassianPullRequestsController.listComments({
663 | 						workspaceSlug: repoInfo.workspaceSlug,
664 | 						repoSlug: repoInfo.repoSlug,
665 | 						// prId: missing
666 | 					} as any),
667 | 				).rejects.toThrow();
668 | 			}
669 | 		}, 10000);
670 | 	});
671 | 
672 | 	// Note: addComment test suite has been removed to avoid creating comments on real PRs during tests
673 | });
674 | 
```

--------------------------------------------------------------------------------
/src/services/vendor.atlassian.pullrequests.service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createAuthMissingError } from '../utils/error.util.js';
  2 | import { Logger } from '../utils/logger.util.js';
  3 | import {
  4 | 	fetchAtlassian,
  5 | 	getAtlassianCredentials,
  6 | } from '../utils/transport.util.js';
  7 | import {
  8 | 	PullRequestDetailed,
  9 | 	PullRequestsResponse,
 10 | 	ListPullRequestsParams,
 11 | 	GetPullRequestParams,
 12 | 	GetPullRequestCommentsParams,
 13 | 	PullRequestCommentsResponse,
 14 | 	PullRequestComment,
 15 | 	CreatePullRequestCommentParams,
 16 | 	CreatePullRequestParams,
 17 | 	UpdatePullRequestParams,
 18 | 	ApprovePullRequestParams,
 19 | 	RejectPullRequestParams,
 20 | 	PullRequestParticipant,
 21 | 	DiffstatResponse,
 22 | } from './vendor.atlassian.pullrequests.types.js';
 23 | 
 24 | /**
 25 |  * Base API path for Bitbucket REST API v2
 26 |  * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/
 27 |  * @constant {string}
 28 |  */
 29 | const API_PATH = '/2.0';
 30 | 
 31 | /**
 32 |  * @namespace VendorAtlassianPullRequestsService
 33 |  * @description Service for interacting with Bitbucket Pull Requests API.
 34 |  * Provides methods for listing pull requests and retrieving pull request details.
 35 |  * All methods require valid Atlassian credentials configured in the environment.
 36 |  */
 37 | 
 38 | // Create a contextualized logger for this file
 39 | const serviceLogger = Logger.forContext(
 40 | 	'services/vendor.atlassian.pullrequests.service.ts',
 41 | );
 42 | 
 43 | // Log service initialization
 44 | serviceLogger.debug('Bitbucket pull requests service initialized');
 45 | 
 46 | /**
 47 |  * List pull requests for a repository
 48 |  * @param {ListPullRequestsParams} params - Parameters for the request
 49 |  * @param {string} params.workspace - The workspace slug or UUID
 50 |  * @param {string} params.repo_slug - The repository slug or UUID
 51 |  * @param {PullRequestState | PullRequestState[]} [params.state] - Filter by pull request state (default: 'OPEN')
 52 |  * @param {string} [params.q] - Query string to filter pull requests
 53 |  * @param {string} [params.sort] - Property to sort by (e.g., 'created_on', '-updated_on')
 54 |  * @param {number} [params.page] - Page number for pagination
 55 |  * @param {number} [params.pagelen] - Number of items per page
 56 |  * @returns {Promise<PullRequestsResponse>} Response containing pull requests
 57 |  * @example
 58 |  * ```typescript
 59 |  * // List open pull requests in a repository, sorted by creation date
 60 |  * const response = await list({
 61 |  *   workspace: 'myworkspace',
 62 |  *   repo_slug: 'myrepo',
 63 |  *   sort: '-created_on',
 64 |  *   pagelen: 25
 65 |  * });
 66 |  * ```
 67 |  */
 68 | async function list(
 69 | 	params: ListPullRequestsParams,
 70 | ): Promise<PullRequestsResponse> {
 71 | 	const methodLogger = Logger.forContext(
 72 | 		'services/vendor.atlassian.pullrequests.service.ts',
 73 | 		'list',
 74 | 	);
 75 | 	methodLogger.debug('Listing Bitbucket pull requests with params:', params);
 76 | 
 77 | 	if (!params.workspace || !params.repo_slug) {
 78 | 		throw new Error('Both workspace and repo_slug parameters are required');
 79 | 	}
 80 | 
 81 | 	const credentials = getAtlassianCredentials();
 82 | 	if (!credentials) {
 83 | 		throw createAuthMissingError(
 84 | 			'Atlassian credentials are required for this operation',
 85 | 		);
 86 | 	}
 87 | 
 88 | 	// Construct query parameters
 89 | 	const queryParams = new URLSearchParams();
 90 | 
 91 | 	// Add state parameter(s) - default to OPEN if not specified
 92 | 	if (params.state) {
 93 | 		if (Array.isArray(params.state)) {
 94 | 			// For multiple states, repeat the parameter
 95 | 			params.state.forEach((state) => {
 96 | 				queryParams.append('state', state);
 97 | 			});
 98 | 		} else {
 99 | 			queryParams.set('state', params.state);
100 | 		}
101 | 	}
102 | 
103 | 	// Add optional query parameters
104 | 	if (params.q) {
105 | 		queryParams.set('q', params.q);
106 | 	}
107 | 	if (params.sort) {
108 | 		queryParams.set('sort', params.sort);
109 | 	}
110 | 	if (params.pagelen) {
111 | 		queryParams.set('pagelen', params.pagelen.toString());
112 | 	}
113 | 	if (params.page) {
114 | 		queryParams.set('page', params.page.toString());
115 | 	}
116 | 
117 | 	const queryString = queryParams.toString()
118 | 		? `?${queryParams.toString()}`
119 | 		: '';
120 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests${queryString}`;
121 | 
122 | 	methodLogger.debug(`Sending request to: ${path}`);
123 | 	return fetchAtlassian<PullRequestsResponse>(credentials, path);
124 | }
125 | 
126 | /**
127 |  * Get detailed information about a specific Bitbucket pull request
128 |  *
129 |  * Retrieves comprehensive details about a single pull request.
130 |  *
131 |  * @async
132 |  * @memberof VendorAtlassianPullRequestsService
133 |  * @param {GetPullRequestParams} params - Parameters for the request
134 |  * @param {string} params.workspace - The workspace slug or UUID
135 |  * @param {string} params.repo_slug - The repository slug or UUID
136 |  * @param {number} params.pull_request_id - The ID of the pull request
137 |  * @returns {Promise<PullRequestDetailed>} Promise containing the detailed pull request information
138 |  * @throws {Error} If Atlassian credentials are missing or API request fails
139 |  * @example
140 |  * // Get pull request details
141 |  * const pullRequest = await get({
142 |  *   workspace: 'my-workspace',
143 |  *   repo_slug: 'my-repo',
144 |  *   pull_request_id: 123
145 |  * });
146 |  */
147 | async function get(params: GetPullRequestParams): Promise<PullRequestDetailed> {
148 | 	const methodLogger = Logger.forContext(
149 | 		'services/vendor.atlassian.pullrequests.service.ts',
150 | 		'get',
151 | 	);
152 | 	methodLogger.debug(
153 | 		`Getting Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`,
154 | 	);
155 | 
156 | 	// Validate pull_request_id is a positive integer
157 | 	const prId = Number(params.pull_request_id);
158 | 	if (isNaN(prId) || prId <= 0 || !Number.isInteger(prId)) {
159 | 		throw new Error(
160 | 			`Invalid pull request ID: ${params.pull_request_id}. Pull request ID must be a positive integer.`,
161 | 		);
162 | 	}
163 | 
164 | 	if (!params.workspace || !params.repo_slug || !params.pull_request_id) {
165 | 		throw new Error(
166 | 			'workspace, repo_slug, and pull_request_id parameters are all required',
167 | 		);
168 | 	}
169 | 
170 | 	const credentials = getAtlassianCredentials();
171 | 	if (!credentials) {
172 | 		throw createAuthMissingError(
173 | 			'Atlassian credentials are required for this operation',
174 | 		);
175 | 	}
176 | 
177 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}`;
178 | 
179 | 	methodLogger.debug(`Sending request to: ${path}`);
180 | 	return fetchAtlassian<PullRequestDetailed>(credentials, path);
181 | }
182 | 
183 | /**
184 |  * Get comments for a specific Bitbucket pull request
185 |  *
186 |  * Retrieves all comments on a specific pull request, including general comments and
187 |  * inline code review comments. Supports pagination.
188 |  *
189 |  * @async
190 |  * @memberof VendorAtlassianPullRequestsService
191 |  * @param {GetPullRequestCommentsParams} params - Parameters for the request
192 |  * @param {string} params.workspace - The workspace slug or UUID
193 |  * @param {string} params.repo_slug - The repository slug or UUID
194 |  * @param {number} params.pull_request_id - The ID of the pull request
195 |  * @param {number} [params.page] - Page number for pagination
196 |  * @param {number} [params.pagelen] - Number of items per page
197 |  * @returns {Promise<PullRequestCommentsResponse>} Promise containing the pull request comments
198 |  * @throws {Error} If Atlassian credentials are missing or API request fails
199 |  * @example
200 |  * // Get comments for a pull request
201 |  * const comments = await getComments({
202 |  *   workspace: 'my-workspace',
203 |  *   repo_slug: 'my-repo',
204 |  *   pull_request_id: 123,
205 |  *   pagelen: 25
206 |  * });
207 |  */
208 | async function getComments(
209 | 	params: GetPullRequestCommentsParams,
210 | ): Promise<PullRequestCommentsResponse> {
211 | 	const methodLogger = Logger.forContext(
212 | 		'services/vendor.atlassian.pullrequests.service.ts',
213 | 		'getComments',
214 | 	);
215 | 	methodLogger.debug(
216 | 		`Getting comments for Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`,
217 | 	);
218 | 
219 | 	// Validate pull_request_id is a positive integer
220 | 	const prId = Number(params.pull_request_id);
221 | 	if (isNaN(prId) || prId <= 0 || !Number.isInteger(prId)) {
222 | 		throw new Error(
223 | 			`Invalid pull request ID: ${params.pull_request_id}. Pull request ID must be a positive integer.`,
224 | 		);
225 | 	}
226 | 
227 | 	if (!params.workspace || !params.repo_slug || !params.pull_request_id) {
228 | 		throw new Error(
229 | 			'workspace, repo_slug, and pull_request_id parameters are all required',
230 | 		);
231 | 	}
232 | 
233 | 	const credentials = getAtlassianCredentials();
234 | 	if (!credentials) {
235 | 		throw createAuthMissingError(
236 | 			'Atlassian credentials are required for this operation',
237 | 		);
238 | 	}
239 | 
240 | 	// Build query parameters
241 | 	const queryParams = new URLSearchParams();
242 | 
243 | 	// Add pagination parameters if provided
244 | 	if (params.pagelen) {
245 | 		queryParams.set('pagelen', params.pagelen.toString());
246 | 	}
247 | 	if (params.page) {
248 | 		queryParams.set('page', params.page.toString());
249 | 	}
250 | 	// Add sort parameter if provided
251 | 	if (params.sort) {
252 | 		queryParams.set('sort', params.sort);
253 | 	}
254 | 
255 | 	const queryString = queryParams.toString()
256 | 		? `?${queryParams.toString()}`
257 | 		: '';
258 | 
259 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/comments${queryString}`;
260 | 
261 | 	methodLogger.debug(`Sending request to: ${path}`);
262 | 	return fetchAtlassian<PullRequestCommentsResponse>(credentials, path);
263 | }
264 | 
265 | /**
266 |  * Add a comment to a specific Bitbucket pull request
267 |  *
268 |  * Creates a new comment on a pull request, either as a general comment or
269 |  * as an inline code comment attached to a specific file and line.
270 |  *
271 |  * @async
272 |  * @memberof VendorAtlassianPullRequestsService
273 |  * @param {CreatePullRequestCommentParams} params - Parameters for the request
274 |  * @param {string} params.workspace - The workspace slug or UUID
275 |  * @param {string} params.repo_slug - The repository slug or UUID
276 |  * @param {number} params.pull_request_id - The ID of the pull request
277 |  * @param {Object} params.content - The content of the comment
278 |  * @param {string} params.content.raw - The raw text of the comment
279 |  * @param {Object} [params.inline] - Optional inline comment location
280 |  * @param {string} params.inline.path - The file path for the inline comment
281 |  * @param {number} params.inline.to - The line number in the file
282 |  * @returns {Promise<PullRequestComment>} Promise containing the created comment
283 |  * @throws {Error} If Atlassian credentials are missing or API request fails
284 |  * @example
285 |  * // Add a general comment to a pull request
286 |  * const comment = await addComment({
287 |  *   workspace: 'my-workspace',
288 |  *   repo_slug: 'my-repo',
289 |  *   pull_request_id: 123,
290 |  *   content: { raw: "This looks good to me!" }
291 |  * });
292 |  *
293 |  * // Add an inline code comment
294 |  * const comment = await addComment({
295 |  *   workspace: 'my-workspace',
296 |  *   repo_slug: 'my-repo',
297 |  *   pull_request_id: 123,
298 |  *   content: { raw: "Consider using a constant here instead." },
299 |  *   inline: { path: "src/main.js", to: 42 }
300 |  * });
301 |  */
302 | async function createComment(
303 | 	params: CreatePullRequestCommentParams,
304 | ): Promise<PullRequestComment> {
305 | 	const methodLogger = Logger.forContext(
306 | 		'services/vendor.atlassian.pullrequests.service.ts',
307 | 		'createComment',
308 | 	);
309 | 	methodLogger.debug(
310 | 		`Creating comment on Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`,
311 | 	);
312 | 
313 | 	if (!params.workspace || !params.repo_slug || !params.pull_request_id) {
314 | 		throw new Error(
315 | 			'workspace, repo_slug, and pull_request_id parameters are all required',
316 | 		);
317 | 	}
318 | 
319 | 	if (!params.content || !params.content.raw) {
320 | 		throw new Error('Comment content is required');
321 | 	}
322 | 
323 | 	const credentials = getAtlassianCredentials();
324 | 	if (!credentials) {
325 | 		throw createAuthMissingError(
326 | 			'Atlassian credentials are required for this operation',
327 | 		);
328 | 	}
329 | 
330 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/comments`;
331 | 
332 | 	methodLogger.debug(`Sending POST request to: ${path}`);
333 | 	return fetchAtlassian<PullRequestComment>(credentials, path, {
334 | 		method: 'POST',
335 | 		body: {
336 | 			content: params.content,
337 | 			inline: params.inline,
338 | 			parent: params.parent,
339 | 		},
340 | 	});
341 | }
342 | 
343 | /**
344 |  * Create a new pull request
345 |  * @param {CreatePullRequestParams} params - Parameters for the request
346 |  * @param {string} params.workspace - The workspace slug or UUID
347 |  * @param {string} params.repo_slug - The repository slug or UUID
348 |  * @param {string} params.title - Title of the pull request
349 |  * @param {string} params.source.branch.name - Source branch name
350 |  * @param {string} params.destination.branch.name - Destination branch name (defaults to main/master)
351 |  * @param {string} [params.description] - Optional description for the pull request
352 |  * @param {boolean} [params.close_source_branch] - Whether to close the source branch after merge (default: false)
353 |  * @returns {Promise<PullRequestDetailed>} Detailed information about the created pull request
354 |  * @example
355 |  * ```typescript
356 |  * // Create a new pull request
357 |  * const pullRequest = await create({
358 |  *   workspace: 'myworkspace',
359 |  *   repo_slug: 'myrepo',
360 |  *   title: 'Add new feature',
361 |  *   source: {
362 |  *     branch: {
363 |  *       name: 'feature/new-feature'
364 |  *     }
365 |  *   },
366 |  *   destination: {
367 |  *     branch: {
368 |  *       name: 'main'
369 |  *     }
370 |  *   },
371 |  *   description: 'This PR adds a new feature...',
372 |  *   close_source_branch: true
373 |  * });
374 |  * ```
375 |  */
376 | async function create(
377 | 	params: CreatePullRequestParams,
378 | ): Promise<PullRequestDetailed> {
379 | 	const methodLogger = Logger.forContext(
380 | 		'services/vendor.atlassian.pullrequests.service.ts',
381 | 		'create',
382 | 	);
383 | 	methodLogger.debug(
384 | 		'Creating new Bitbucket pull request with params:',
385 | 		params,
386 | 	);
387 | 
388 | 	if (!params.workspace || !params.repo_slug) {
389 | 		throw new Error('Both workspace and repo_slug parameters are required');
390 | 	}
391 | 
392 | 	if (!params.title) {
393 | 		throw new Error('Pull request title is required');
394 | 	}
395 | 
396 | 	if (!params.source || !params.source.branch || !params.source.branch.name) {
397 | 		throw new Error('Source branch name is required');
398 | 	}
399 | 
400 | 	// Destination branch is required but may have a default
401 | 	if (
402 | 		!params.destination ||
403 | 		!params.destination.branch ||
404 | 		!params.destination.branch.name
405 | 	) {
406 | 		throw new Error('Destination branch name is required');
407 | 	}
408 | 
409 | 	const credentials = getAtlassianCredentials();
410 | 	if (!credentials) {
411 | 		throw createAuthMissingError(
412 | 			'Atlassian credentials are required for this operation',
413 | 		);
414 | 	}
415 | 
416 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests`;
417 | 
418 | 	// Construct request body with only the fields needed by the API
419 | 	const requestBody = {
420 | 		title: params.title,
421 | 		source: {
422 | 			branch: {
423 | 				name: params.source.branch.name,
424 | 			},
425 | 		},
426 | 		destination: {
427 | 			branch: {
428 | 				name: params.destination.branch.name,
429 | 			},
430 | 		},
431 | 		description: params.description || '',
432 | 		close_source_branch: !!params.close_source_branch,
433 | 	};
434 | 
435 | 	methodLogger.debug(`Sending POST request to: ${path}`);
436 | 	return fetchAtlassian<PullRequestDetailed>(credentials, path, {
437 | 		method: 'POST',
438 | 		body: requestBody,
439 | 	});
440 | }
441 | 
442 | /**
443 |  * Get raw diff content for a specific Bitbucket pull request
444 |  *
445 |  * Retrieves the raw diff content showing actual code changes in the pull request.
446 |  * The diff is returned in a standard unified diff format.
447 |  *
448 |  * @async
449 |  * @memberof VendorAtlassianPullRequestsService
450 |  * @param {GetPullRequestParams} params - Parameters for the request
451 |  * @param {string} params.workspace - The workspace slug or UUID
452 |  * @param {string} params.repo_slug - The repository slug or UUID
453 |  * @param {number} params.pull_request_id - The ID of the pull request
454 |  * @returns {Promise<string>} Promise containing the raw diff content
455 |  * @throws {Error} If Atlassian credentials are missing or API request fails
456 |  * @example
457 |  * // Get raw diff content for a pull request
458 |  * const diffContent = await getRawDiff({
459 |  *   workspace: 'my-workspace',
460 |  *   repo_slug: 'my-repo',
461 |  *   pull_request_id: 123
462 |  * });
463 |  */
464 | async function getRawDiff(params: GetPullRequestParams): Promise<string> {
465 | 	const methodLogger = Logger.forContext(
466 | 		'services/vendor.atlassian.pullrequests.service.ts',
467 | 		'getRawDiff',
468 | 	);
469 | 	methodLogger.debug(
470 | 		`Getting raw diff for Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`,
471 | 	);
472 | 
473 | 	if (!params.workspace || !params.repo_slug || !params.pull_request_id) {
474 | 		throw new Error(
475 | 			'workspace, repo_slug, and pull_request_id parameters are all required',
476 | 		);
477 | 	}
478 | 
479 | 	const credentials = getAtlassianCredentials();
480 | 	if (!credentials) {
481 | 		throw createAuthMissingError(
482 | 			'Atlassian credentials are required for this operation',
483 | 		);
484 | 	}
485 | 
486 | 	// Use the diff endpoint directly
487 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/diff`;
488 | 
489 | 	methodLogger.debug(`Sending request to: ${path}`);
490 | 
491 | 	// Override the Accept header to get raw diff content instead of JSON
492 | 	// The transport utility handles the text response automatically
493 | 	return fetchAtlassian<string>(credentials, path, {
494 | 		headers: {
495 | 			Accept: 'text/plain',
496 | 		},
497 | 	});
498 | }
499 | 
500 | /**
501 |  * Get the diffstat information for a pull request
502 |  *
503 |  * Returns summary statistics about the changes in a pull request,
504 |  * including files changed, insertions, and deletions.
505 |  *
506 |  * @async
507 |  * @memberof VendorAtlassianPullRequestsService
508 |  * @param {GetPullRequestParams} params - Parameters for the request
509 |  * @returns {Promise<DiffstatResponse>} Promise containing the diffstat response
510 |  */
511 | async function getDiffstat(
512 | 	params: GetPullRequestParams,
513 | ): Promise<DiffstatResponse> {
514 | 	const methodLogger = Logger.forContext(
515 | 		'services/vendor.atlassian.pullrequests.service.ts',
516 | 		'getDiffstat',
517 | 	);
518 | 	methodLogger.debug(
519 | 		`Getting diffstat for Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`,
520 | 	);
521 | 
522 | 	if (!params.workspace || !params.repo_slug || !params.pull_request_id) {
523 | 		throw new Error(
524 | 			'workspace, repo_slug, and pull_request_id parameters are all required',
525 | 		);
526 | 	}
527 | 
528 | 	const credentials = getAtlassianCredentials();
529 | 	if (!credentials) {
530 | 		throw createAuthMissingError(
531 | 			'Atlassian credentials are required for this operation',
532 | 		);
533 | 	}
534 | 
535 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/diffstat`;
536 | 
537 | 	methodLogger.debug(`Sending request to: ${path}`);
538 | 	return fetchAtlassian<DiffstatResponse>(credentials, path);
539 | }
540 | 
541 | /**
542 |  * Fetches the raw diff content from a specific Bitbucket API URL.
543 |  * Used primarily for fetching file-specific diffs linked from inline comments.
544 |  *
545 |  * @async
546 |  * @param {string} url - The exact Bitbucket API URL to fetch the diff from.
547 |  * @returns {Promise<string>} Promise containing the raw diff content as a string.
548 |  * @throws {Error} If Atlassian credentials are missing or the API request fails.
549 |  */
550 | async function getDiffForUrl(url: string): Promise<string> {
551 | 	const methodLogger = Logger.forContext(
552 | 		'services/vendor.atlassian.pullrequests.service.ts',
553 | 		'getDiffForUrl',
554 | 	);
555 | 	methodLogger.debug(`Getting raw diff from URL: ${url}`);
556 | 
557 | 	if (!url) {
558 | 		throw new Error('URL parameter is required');
559 | 	}
560 | 
561 | 	const credentials = getAtlassianCredentials();
562 | 	if (!credentials) {
563 | 		throw createAuthMissingError(
564 | 			'Atlassian credentials are required for this operation',
565 | 		);
566 | 	}
567 | 
568 | 	// Parse the provided URL to extract the path and query string
569 | 	let urlPath = '';
570 | 	try {
571 | 		const parsedUrl = new URL(url);
572 | 		urlPath = parsedUrl.pathname + parsedUrl.search;
573 | 		methodLogger.debug(`Parsed path and query from URL: ${urlPath}`);
574 | 	} catch (parseError) {
575 | 		methodLogger.error(`Failed to parse URL: ${url}`, parseError);
576 | 		throw new Error(`Invalid URL provided for diff fetching: ${url}`);
577 | 	}
578 | 
579 | 	// Pass only the path part to fetchAtlassian
580 | 	// Ensure the Accept header requests plain text for the raw diff.
581 | 	return fetchAtlassian<string>(credentials, urlPath, {
582 | 		headers: {
583 | 			Accept: 'text/plain',
584 | 		},
585 | 	});
586 | }
587 | 
588 | /**
589 |  * Update an existing pull request
590 |  * @param {UpdatePullRequestParams} params - Parameters for the request
591 |  * @param {string} params.workspace - The workspace slug or UUID
592 |  * @param {string} params.repo_slug - The repository slug or UUID
593 |  * @param {number} params.pull_request_id - The ID of the pull request to update
594 |  * @param {string} [params.title] - Updated title of the pull request
595 |  * @param {string} [params.description] - Updated description for the pull request
596 |  * @returns {Promise<PullRequestDetailed>} Updated pull request information
597 |  * @example
598 |  * ```typescript
599 |  * // Update a pull request's title and description
600 |  * const updatedPR = await update({
601 |  *   workspace: 'myworkspace',
602 |  *   repo_slug: 'myrepo',
603 |  *   pull_request_id: 123,
604 |  *   title: 'Updated Feature Implementation',
605 |  *   description: 'This PR now includes additional improvements...'
606 |  * });
607 |  * ```
608 |  */
609 | async function update(
610 | 	params: UpdatePullRequestParams,
611 | ): Promise<PullRequestDetailed> {
612 | 	const methodLogger = Logger.forContext(
613 | 		'services/vendor.atlassian.pullrequests.service.ts',
614 | 		'update',
615 | 	);
616 | 	methodLogger.debug('Updating Bitbucket pull request with params:', params);
617 | 
618 | 	if (!params.workspace || !params.repo_slug || !params.pull_request_id) {
619 | 		throw new Error(
620 | 			'workspace, repo_slug, and pull_request_id parameters are all required',
621 | 		);
622 | 	}
623 | 
624 | 	// At least one field to update must be provided
625 | 	if (!params.title && !params.description) {
626 | 		throw new Error(
627 | 			'At least one field to update (title or description) must be provided',
628 | 		);
629 | 	}
630 | 
631 | 	const credentials = getAtlassianCredentials();
632 | 	if (!credentials) {
633 | 		throw createAuthMissingError(
634 | 			'Atlassian credentials are required for this operation',
635 | 		);
636 | 	}
637 | 
638 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}`;
639 | 
640 | 	// Construct request body with only the fields to update
641 | 	const requestBody: Record<string, unknown> = {};
642 | 	if (params.title !== undefined) {
643 | 		requestBody.title = params.title;
644 | 	}
645 | 	if (params.description !== undefined) {
646 | 		requestBody.description = params.description;
647 | 	}
648 | 
649 | 	methodLogger.debug(`Sending PUT request to: ${path}`);
650 | 	return fetchAtlassian<PullRequestDetailed>(credentials, path, {
651 | 		method: 'PUT',
652 | 		body: requestBody,
653 | 	});
654 | }
655 | 
656 | /**
657 |  * Approve a pull request
658 |  * @param {ApprovePullRequestParams} params - Parameters for the request
659 |  * @param {string} params.workspace - The workspace slug or UUID
660 |  * @param {string} params.repo_slug - The repository slug or UUID
661 |  * @param {number} params.pull_request_id - The ID of the pull request to approve
662 |  * @returns {Promise<PullRequestParticipant>} Participant information showing approval status
663 |  * @example
664 |  * ```typescript
665 |  * // Approve a pull request
666 |  * const approval = await approve({
667 |  *   workspace: 'myworkspace',
668 |  *   repo_slug: 'myrepo',
669 |  *   pull_request_id: 123
670 |  * });
671 |  * ```
672 |  */
673 | async function approve(
674 | 	params: ApprovePullRequestParams,
675 | ): Promise<PullRequestParticipant> {
676 | 	const methodLogger = Logger.forContext(
677 | 		'services/vendor.atlassian.pullrequests.service.ts',
678 | 		'approve',
679 | 	);
680 | 	methodLogger.debug(
681 | 		`Approving Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`,
682 | 	);
683 | 
684 | 	if (!params.workspace || !params.repo_slug || !params.pull_request_id) {
685 | 		throw new Error(
686 | 			'workspace, repo_slug, and pull_request_id parameters are all required',
687 | 		);
688 | 	}
689 | 
690 | 	const credentials = getAtlassianCredentials();
691 | 	if (!credentials) {
692 | 		throw createAuthMissingError(
693 | 			'Atlassian credentials are required for this operation',
694 | 		);
695 | 	}
696 | 
697 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/approve`;
698 | 
699 | 	methodLogger.debug(`Sending POST request to: ${path}`);
700 | 	return fetchAtlassian<PullRequestParticipant>(credentials, path, {
701 | 		method: 'POST',
702 | 	});
703 | }
704 | 
705 | /**
706 |  * Request changes on a pull request (reject)
707 |  * @param {RejectPullRequestParams} params - Parameters for the request
708 |  * @param {string} params.workspace - The workspace slug or UUID
709 |  * @param {string} params.repo_slug - The repository slug or UUID
710 |  * @param {number} params.pull_request_id - The ID of the pull request to request changes on
711 |  * @returns {Promise<PullRequestParticipant>} Participant information showing rejection status
712 |  * @example
713 |  * ```typescript
714 |  * // Request changes on a pull request
715 |  * const rejection = await reject({
716 |  *   workspace: 'myworkspace',
717 |  *   repo_slug: 'myrepo',
718 |  *   pull_request_id: 123
719 |  * });
720 |  * ```
721 |  */
722 | async function reject(
723 | 	params: RejectPullRequestParams,
724 | ): Promise<PullRequestParticipant> {
725 | 	const methodLogger = Logger.forContext(
726 | 		'services/vendor.atlassian.pullrequests.service.ts',
727 | 		'reject',
728 | 	);
729 | 	methodLogger.debug(
730 | 		`Requesting changes on Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`,
731 | 	);
732 | 
733 | 	if (!params.workspace || !params.repo_slug || !params.pull_request_id) {
734 | 		throw new Error(
735 | 			'workspace, repo_slug, and pull_request_id parameters are all required',
736 | 		);
737 | 	}
738 | 
739 | 	const credentials = getAtlassianCredentials();
740 | 	if (!credentials) {
741 | 		throw createAuthMissingError(
742 | 			'Atlassian credentials are required for this operation',
743 | 		);
744 | 	}
745 | 
746 | 	const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/request-changes`;
747 | 
748 | 	methodLogger.debug(`Sending POST request to: ${path}`);
749 | 	return fetchAtlassian<PullRequestParticipant>(credentials, path, {
750 | 		method: 'POST',
751 | 	});
752 | }
753 | 
754 | export default {
755 | 	list,
756 | 	get,
757 | 	getComments,
758 | 	createComment,
759 | 	create,
760 | 	update,
761 | 	approve,
762 | 	reject,
763 | 	getRawDiff,
764 | 	getDiffstat,
765 | 	getDiffForUrl,
766 | };
767 | 
```
Page 5/6FirstPrevNextLast