mcp-server-bitbucket 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Pipeline tools for Bitbucket MCP Server
3
+ */
4
+
5
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
6
+ import { getClient } from '../client.js';
7
+ import { validateLimit, notFoundResponse } from '../utils.js';
8
+
9
+ export const definitions: Tool[] = [
10
+ {
11
+ name: 'trigger_pipeline',
12
+ description: 'Trigger a pipeline run. Supports custom pipelines (from "custom:" section in bitbucket-pipelines.yml) and commit-based triggers.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ repo_slug: { type: 'string', description: 'Repository slug' },
17
+ branch: { type: 'string', description: 'Branch to run pipeline on (default: main). Mutually exclusive with commit.', default: 'main' },
18
+ commit: { type: 'string', description: 'Commit hash to run pipeline on. Mutually exclusive with branch.' },
19
+ custom_pipeline: { type: 'string', description: 'Name of custom pipeline from "custom:" section (e.g., "deploy-staging", "dry-run")' },
20
+ variables: {
21
+ type: 'array',
22
+ description: 'Pipeline variables. Can be array of {key, value, secured?} or simple {key: value} object for backwards compatibility.',
23
+ items: {
24
+ type: 'object',
25
+ properties: {
26
+ key: { type: 'string', description: 'Variable name' },
27
+ value: { type: 'string', description: 'Variable value' },
28
+ secured: { type: 'boolean', description: 'Whether to mark as secured (encrypted)', default: false },
29
+ },
30
+ required: ['key', 'value'],
31
+ },
32
+ },
33
+ },
34
+ required: ['repo_slug'],
35
+ },
36
+ },
37
+ {
38
+ name: 'get_pipeline',
39
+ description: 'Get status of a pipeline run.',
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: {
43
+ repo_slug: { type: 'string', description: 'Repository slug' },
44
+ pipeline_uuid: { type: 'string', description: 'Pipeline UUID' },
45
+ },
46
+ required: ['repo_slug', 'pipeline_uuid'],
47
+ },
48
+ },
49
+ {
50
+ name: 'list_pipelines',
51
+ description: 'List recent pipeline runs for a repository.',
52
+ inputSchema: {
53
+ type: 'object',
54
+ properties: {
55
+ repo_slug: { type: 'string', description: 'Repository slug' },
56
+ limit: { type: 'number', description: 'Maximum results (default: 10)', default: 10 },
57
+ },
58
+ required: ['repo_slug'],
59
+ },
60
+ },
61
+ {
62
+ name: 'get_pipeline_logs',
63
+ description: 'Get logs for a pipeline run. If step_uuid is not provided, returns list of steps.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ repo_slug: { type: 'string', description: 'Repository slug' },
68
+ pipeline_uuid: { type: 'string', description: 'Pipeline UUID' },
69
+ step_uuid: { type: 'string', description: 'Step UUID (optional, get from steps list first)' },
70
+ },
71
+ required: ['repo_slug', 'pipeline_uuid'],
72
+ },
73
+ },
74
+ {
75
+ name: 'stop_pipeline',
76
+ description: 'Stop a running pipeline.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ repo_slug: { type: 'string', description: 'Repository slug' },
81
+ pipeline_uuid: { type: 'string', description: 'Pipeline UUID' },
82
+ },
83
+ required: ['repo_slug', 'pipeline_uuid'],
84
+ },
85
+ },
86
+ {
87
+ name: 'list_pipeline_variables',
88
+ description: 'List pipeline variables for a repository.',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ repo_slug: { type: 'string', description: 'Repository slug' },
93
+ limit: { type: 'number', description: 'Maximum results (default: 50)', default: 50 },
94
+ },
95
+ required: ['repo_slug'],
96
+ },
97
+ },
98
+ {
99
+ name: 'get_pipeline_variable',
100
+ description: 'Get details about a specific pipeline variable.',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ repo_slug: { type: 'string', description: 'Repository slug' },
105
+ variable_uuid: { type: 'string', description: 'Variable UUID' },
106
+ },
107
+ required: ['repo_slug', 'variable_uuid'],
108
+ },
109
+ },
110
+ {
111
+ name: 'create_pipeline_variable',
112
+ description: 'Create a pipeline variable.',
113
+ inputSchema: {
114
+ type: 'object',
115
+ properties: {
116
+ repo_slug: { type: 'string', description: 'Repository slug' },
117
+ key: { type: 'string', description: 'Variable name' },
118
+ value: { type: 'string', description: 'Variable value' },
119
+ secured: { type: 'boolean', description: 'Encrypt the value (secured variables cannot be read back)', default: false },
120
+ },
121
+ required: ['repo_slug', 'key', 'value'],
122
+ },
123
+ },
124
+ {
125
+ name: 'update_pipeline_variable',
126
+ description: "Update a pipeline variable's value.",
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ repo_slug: { type: 'string', description: 'Repository slug' },
131
+ variable_uuid: { type: 'string', description: 'Variable UUID' },
132
+ value: { type: 'string', description: 'New variable value' },
133
+ },
134
+ required: ['repo_slug', 'variable_uuid', 'value'],
135
+ },
136
+ },
137
+ {
138
+ name: 'delete_pipeline_variable',
139
+ description: 'Delete a pipeline variable.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ repo_slug: { type: 'string', description: 'Repository slug' },
144
+ variable_uuid: { type: 'string', description: 'Variable UUID' },
145
+ },
146
+ required: ['repo_slug', 'variable_uuid'],
147
+ },
148
+ },
149
+ ];
150
+
151
+ export const handlers: Record<string, (args: Record<string, unknown>) => Promise<Record<string, unknown>>> = {
152
+ trigger_pipeline: async (args) => {
153
+ const client = getClient();
154
+ const result = await client.triggerPipeline(args.repo_slug as string, {
155
+ branch: args.branch as string | undefined,
156
+ commit: args.commit as string | undefined,
157
+ customPipeline: args.custom_pipeline as string | undefined,
158
+ variables: args.variables as { key: string; value: string; secured?: boolean }[] | Record<string, string> | undefined,
159
+ });
160
+ return {
161
+ uuid: result.uuid,
162
+ build_number: result.build_number,
163
+ state: result.state?.name,
164
+ };
165
+ },
166
+
167
+ get_pipeline: async (args) => {
168
+ const client = getClient();
169
+ const result = await client.getPipeline(args.repo_slug as string, args.pipeline_uuid as string);
170
+ if (!result) {
171
+ return notFoundResponse('Pipeline', args.pipeline_uuid as string);
172
+ }
173
+ return {
174
+ uuid: result.uuid,
175
+ build_number: result.build_number,
176
+ state: result.state?.name,
177
+ result: result.state?.result?.name,
178
+ branch: result.target?.ref_name,
179
+ created: result.created_on,
180
+ completed: result.completed_on,
181
+ duration: result.duration_in_seconds,
182
+ };
183
+ },
184
+
185
+ list_pipelines: async (args) => {
186
+ const client = getClient();
187
+ const pipelines = await client.listPipelines(args.repo_slug as string, {
188
+ limit: validateLimit((args.limit as number) || 10),
189
+ });
190
+ return {
191
+ pipelines: pipelines.map(p => ({
192
+ uuid: p.uuid,
193
+ build_number: p.build_number,
194
+ state: p.state?.name,
195
+ result: p.state?.result?.name,
196
+ branch: p.target?.ref_name,
197
+ created: p.created_on,
198
+ })),
199
+ };
200
+ },
201
+
202
+ get_pipeline_logs: async (args) => {
203
+ const client = getClient();
204
+ const pipelineUuid = args.pipeline_uuid as string;
205
+ const stepUuid = args.step_uuid as string | undefined;
206
+
207
+ if (!stepUuid) {
208
+ const steps = await client.getPipelineSteps(args.repo_slug as string, pipelineUuid);
209
+ return {
210
+ message: 'Provide step_uuid to get logs for a specific step',
211
+ steps: steps.map(s => ({
212
+ uuid: s.uuid,
213
+ name: s.name,
214
+ state: s.state?.name,
215
+ result: s.state?.result?.name,
216
+ duration: s.duration_in_seconds,
217
+ })),
218
+ };
219
+ }
220
+
221
+ const logs = await client.getPipelineLogs(args.repo_slug as string, pipelineUuid, stepUuid);
222
+ return {
223
+ step_uuid: stepUuid,
224
+ logs: logs || '(no logs available)',
225
+ };
226
+ },
227
+
228
+ stop_pipeline: async (args) => {
229
+ const client = getClient();
230
+ const result = await client.stopPipeline(args.repo_slug as string, args.pipeline_uuid as string);
231
+ return {
232
+ uuid: result.uuid,
233
+ state: result.state?.name,
234
+ };
235
+ },
236
+
237
+ list_pipeline_variables: async (args) => {
238
+ const client = getClient();
239
+ const variables = await client.listPipelineVariables(args.repo_slug as string, {
240
+ limit: validateLimit((args.limit as number) || 50),
241
+ });
242
+ return {
243
+ variables: variables.map(v => ({
244
+ uuid: v.uuid,
245
+ key: v.key,
246
+ secured: v.secured,
247
+ value: v.secured ? undefined : v.value,
248
+ })),
249
+ };
250
+ },
251
+
252
+ get_pipeline_variable: async (args) => {
253
+ const client = getClient();
254
+ const result = await client.getPipelineVariable(args.repo_slug as string, args.variable_uuid as string);
255
+ if (!result) {
256
+ return notFoundResponse('Pipeline variable', args.variable_uuid as string);
257
+ }
258
+ return {
259
+ uuid: result.uuid,
260
+ key: result.key,
261
+ secured: result.secured,
262
+ value: result.secured ? undefined : result.value,
263
+ };
264
+ },
265
+
266
+ create_pipeline_variable: async (args) => {
267
+ const client = getClient();
268
+ const result = await client.createPipelineVariable(
269
+ args.repo_slug as string,
270
+ args.key as string,
271
+ args.value as string,
272
+ args.secured as boolean ?? false
273
+ );
274
+ return {
275
+ uuid: result.uuid,
276
+ key: result.key,
277
+ secured: result.secured,
278
+ };
279
+ },
280
+
281
+ update_pipeline_variable: async (args) => {
282
+ const client = getClient();
283
+ const result = await client.updatePipelineVariable(
284
+ args.repo_slug as string,
285
+ args.variable_uuid as string,
286
+ args.value as string
287
+ );
288
+ return {
289
+ uuid: result.uuid,
290
+ key: result.key,
291
+ secured: result.secured,
292
+ };
293
+ },
294
+
295
+ delete_pipeline_variable: async (args) => {
296
+ const client = getClient();
297
+ await client.deletePipelineVariable(args.repo_slug as string, args.variable_uuid as string);
298
+ return {};
299
+ },
300
+ };
301
+
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Project tools for Bitbucket MCP Server
3
+ */
4
+
5
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
6
+ import { getClient } from '../client.js';
7
+ import { validateLimit, notFoundResponse } from '../utils.js';
8
+
9
+ export const definitions: Tool[] = [
10
+ {
11
+ name: 'list_projects',
12
+ description: 'List projects in the workspace.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ limit: { type: 'number', description: 'Maximum results (default: 50)', default: 50 },
17
+ },
18
+ required: [],
19
+ },
20
+ },
21
+ {
22
+ name: 'get_project',
23
+ description: 'Get information about a specific project.',
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ project_key: { type: 'string', description: 'Project key (e.g., "DS", "PROJ")' },
28
+ },
29
+ required: ['project_key'],
30
+ },
31
+ },
32
+ ];
33
+
34
+ export const handlers: Record<string, (args: Record<string, unknown>) => Promise<Record<string, unknown>>> = {
35
+ list_projects: async (args) => {
36
+ const client = getClient();
37
+ const projects = await client.listProjects({
38
+ limit: validateLimit((args.limit as number) || 50),
39
+ });
40
+ return {
41
+ projects: projects.map(p => ({
42
+ key: p.key,
43
+ name: p.name,
44
+ description: p.description,
45
+ })),
46
+ };
47
+ },
48
+
49
+ get_project: async (args) => {
50
+ const client = getClient();
51
+ const result = await client.getProject(args.project_key as string);
52
+ if (!result) {
53
+ return notFoundResponse('Project', args.project_key as string);
54
+ }
55
+ return {
56
+ key: result.key,
57
+ name: result.name,
58
+ description: result.description,
59
+ uuid: result.uuid,
60
+ };
61
+ },
62
+ };
63
+
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Pull Request tools for Bitbucket MCP Server
3
+ */
4
+
5
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
6
+ import { getClient } from '../client.js';
7
+ import { validateLimit, notFoundResponse } from '../utils.js';
8
+ import { PRState, MergeStrategy } from '../types.js';
9
+
10
+ export const definitions: Tool[] = [
11
+ {
12
+ name: 'create_pull_request',
13
+ description: 'Create a pull request in a Bitbucket repository.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ repo_slug: { type: 'string', description: 'Repository slug' },
18
+ title: { type: 'string', description: 'PR title' },
19
+ source_branch: { type: 'string', description: 'Source branch name' },
20
+ destination_branch: { type: 'string', description: 'Target branch (default: main)', default: 'main' },
21
+ description: { type: 'string', description: 'PR description in markdown', default: '' },
22
+ close_source_branch: { type: 'boolean', description: 'Delete source branch after merge', default: true },
23
+ },
24
+ required: ['repo_slug', 'title', 'source_branch'],
25
+ },
26
+ },
27
+ {
28
+ name: 'get_pull_request',
29
+ description: 'Get information about a pull request.',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ repo_slug: { type: 'string', description: 'Repository slug' },
34
+ pr_id: { type: 'number', description: 'Pull request ID' },
35
+ },
36
+ required: ['repo_slug', 'pr_id'],
37
+ },
38
+ },
39
+ {
40
+ name: 'list_pull_requests',
41
+ description: 'List pull requests in a repository.',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ repo_slug: { type: 'string', description: 'Repository slug' },
46
+ state: { type: 'string', description: 'Filter by state: OPEN, MERGED, DECLINED, SUPERSEDED', default: 'OPEN' },
47
+ limit: { type: 'number', description: 'Maximum results (default: 20, max: 100)', default: 20 },
48
+ },
49
+ required: ['repo_slug'],
50
+ },
51
+ },
52
+ {
53
+ name: 'merge_pull_request',
54
+ description: 'Merge a pull request.',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ repo_slug: { type: 'string', description: 'Repository slug' },
59
+ pr_id: { type: 'number', description: 'Pull request ID' },
60
+ merge_strategy: { type: 'string', description: 'merge_commit, squash, or fast_forward', default: 'merge_commit' },
61
+ close_source_branch: { type: 'boolean', description: 'Delete source branch after merge', default: true },
62
+ message: { type: 'string', description: 'Optional merge commit message' },
63
+ },
64
+ required: ['repo_slug', 'pr_id'],
65
+ },
66
+ },
67
+ {
68
+ name: 'list_pr_comments',
69
+ description: 'List comments on a pull request.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ repo_slug: { type: 'string', description: 'Repository slug' },
74
+ pr_id: { type: 'number', description: 'Pull request ID' },
75
+ limit: { type: 'number', description: 'Maximum results (default: 50)', default: 50 },
76
+ },
77
+ required: ['repo_slug', 'pr_id'],
78
+ },
79
+ },
80
+ {
81
+ name: 'add_pr_comment',
82
+ description: 'Add a comment to a pull request. Can add general or inline comments.',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {
86
+ repo_slug: { type: 'string', description: 'Repository slug' },
87
+ pr_id: { type: 'number', description: 'Pull request ID' },
88
+ content: { type: 'string', description: 'Comment content (markdown supported)' },
89
+ file_path: { type: 'string', description: 'File path for inline comment (optional)' },
90
+ line: { type: 'number', description: 'Line number for inline comment (optional, requires file_path)' },
91
+ },
92
+ required: ['repo_slug', 'pr_id', 'content'],
93
+ },
94
+ },
95
+ {
96
+ name: 'approve_pr',
97
+ description: 'Approve a pull request.',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ repo_slug: { type: 'string', description: 'Repository slug' },
102
+ pr_id: { type: 'number', description: 'Pull request ID' },
103
+ },
104
+ required: ['repo_slug', 'pr_id'],
105
+ },
106
+ },
107
+ {
108
+ name: 'unapprove_pr',
109
+ description: 'Remove your approval from a pull request.',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ repo_slug: { type: 'string', description: 'Repository slug' },
114
+ pr_id: { type: 'number', description: 'Pull request ID' },
115
+ },
116
+ required: ['repo_slug', 'pr_id'],
117
+ },
118
+ },
119
+ {
120
+ name: 'request_changes_pr',
121
+ description: 'Request changes on a pull request.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ repo_slug: { type: 'string', description: 'Repository slug' },
126
+ pr_id: { type: 'number', description: 'Pull request ID' },
127
+ },
128
+ required: ['repo_slug', 'pr_id'],
129
+ },
130
+ },
131
+ {
132
+ name: 'decline_pr',
133
+ description: 'Decline (close without merging) a pull request.',
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ repo_slug: { type: 'string', description: 'Repository slug' },
138
+ pr_id: { type: 'number', description: 'Pull request ID' },
139
+ },
140
+ required: ['repo_slug', 'pr_id'],
141
+ },
142
+ },
143
+ {
144
+ name: 'get_pr_diff',
145
+ description: 'Get the diff of a pull request.',
146
+ inputSchema: {
147
+ type: 'object',
148
+ properties: {
149
+ repo_slug: { type: 'string', description: 'Repository slug' },
150
+ pr_id: { type: 'number', description: 'Pull request ID' },
151
+ },
152
+ required: ['repo_slug', 'pr_id'],
153
+ },
154
+ },
155
+ ];
156
+
157
+ export const handlers: Record<string, (args: Record<string, unknown>) => Promise<Record<string, unknown>>> = {
158
+ create_pull_request: async (args) => {
159
+ const client = getClient();
160
+ const result = await client.createPullRequest(args.repo_slug as string, {
161
+ title: args.title as string,
162
+ sourceBranch: args.source_branch as string,
163
+ destinationBranch: args.destination_branch as string || 'main',
164
+ description: args.description as string,
165
+ closeSourceBranch: args.close_source_branch as boolean ?? true,
166
+ });
167
+ return {
168
+ id: result.id,
169
+ title: result.title,
170
+ state: result.state,
171
+ url: client.extractPrUrl(result),
172
+ };
173
+ },
174
+
175
+ get_pull_request: async (args) => {
176
+ const client = getClient();
177
+ const result = await client.getPullRequest(args.repo_slug as string, args.pr_id as number);
178
+ if (!result) {
179
+ return notFoundResponse('PR', `#${args.pr_id}`);
180
+ }
181
+ return {
182
+ id: result.id,
183
+ title: result.title,
184
+ description: result.description,
185
+ state: result.state,
186
+ author: result.author?.display_name,
187
+ source_branch: result.source?.branch?.name,
188
+ destination_branch: result.destination?.branch?.name,
189
+ reviewers: result.reviewers?.map(r => r.display_name) || [],
190
+ url: client.extractPrUrl(result),
191
+ created: result.created_on,
192
+ updated: result.updated_on,
193
+ };
194
+ },
195
+
196
+ list_pull_requests: async (args) => {
197
+ const client = getClient();
198
+ const state = (args.state as string || 'OPEN').toUpperCase();
199
+ const validState = Object.values(PRState).includes(state as PRState) ? state : 'OPEN';
200
+
201
+ const prs = await client.listPullRequests(args.repo_slug as string, {
202
+ state: validState,
203
+ limit: validateLimit((args.limit as number) || 20),
204
+ });
205
+ return {
206
+ pull_requests: prs.map(pr => ({
207
+ id: pr.id,
208
+ title: pr.title,
209
+ state: pr.state,
210
+ author: pr.author?.display_name,
211
+ source_branch: pr.source?.branch?.name,
212
+ destination_branch: pr.destination?.branch?.name,
213
+ url: client.extractPrUrl(pr),
214
+ })),
215
+ };
216
+ },
217
+
218
+ merge_pull_request: async (args) => {
219
+ const client = getClient();
220
+ const strategy = (args.merge_strategy as string || 'merge_commit').toLowerCase();
221
+ const validStrategy = Object.values(MergeStrategy).includes(strategy as MergeStrategy) ? strategy : 'merge_commit';
222
+
223
+ const result = await client.mergePullRequest(args.repo_slug as string, args.pr_id as number, {
224
+ mergeStrategy: validStrategy,
225
+ closeSourceBranch: args.close_source_branch as boolean ?? true,
226
+ message: args.message as string,
227
+ });
228
+ return {
229
+ id: result.id,
230
+ state: result.state,
231
+ merge_commit: result.merge_commit?.hash,
232
+ url: client.extractPrUrl(result),
233
+ };
234
+ },
235
+
236
+ list_pr_comments: async (args) => {
237
+ const client = getClient();
238
+ const comments = await client.listPrComments(args.repo_slug as string, args.pr_id as number, {
239
+ limit: validateLimit((args.limit as number) || 50),
240
+ });
241
+ return {
242
+ pr_id: args.pr_id,
243
+ comments: comments.map(c => ({
244
+ id: c.id,
245
+ content: c.content?.raw || '',
246
+ author: c.user?.display_name,
247
+ created: c.created_on,
248
+ inline: c.inline ? { path: c.inline.path, line: c.inline.to } : undefined,
249
+ })),
250
+ };
251
+ },
252
+
253
+ add_pr_comment: async (args) => {
254
+ const client = getClient();
255
+ let inline: { path: string; to: number } | undefined;
256
+ if (args.file_path && args.line) {
257
+ inline = { path: args.file_path as string, to: args.line as number };
258
+ }
259
+ const result = await client.addPrComment(
260
+ args.repo_slug as string,
261
+ args.pr_id as number,
262
+ args.content as string,
263
+ inline
264
+ );
265
+ return {
266
+ id: result.id,
267
+ content: result.content?.raw || '',
268
+ inline,
269
+ };
270
+ },
271
+
272
+ approve_pr: async (args) => {
273
+ const client = getClient();
274
+ const result = await client.approvePr(args.repo_slug as string, args.pr_id as number);
275
+ return {
276
+ pr_id: args.pr_id,
277
+ approved_by: (result as { user?: { display_name?: string } }).user?.display_name,
278
+ };
279
+ },
280
+
281
+ unapprove_pr: async (args) => {
282
+ const client = getClient();
283
+ await client.unapprovePr(args.repo_slug as string, args.pr_id as number);
284
+ return { pr_id: args.pr_id };
285
+ },
286
+
287
+ request_changes_pr: async (args) => {
288
+ const client = getClient();
289
+ const result = await client.requestChangesPr(args.repo_slug as string, args.pr_id as number);
290
+ return {
291
+ pr_id: args.pr_id,
292
+ requested_by: (result as { user?: { display_name?: string } }).user?.display_name,
293
+ };
294
+ },
295
+
296
+ decline_pr: async (args) => {
297
+ const client = getClient();
298
+ const result = await client.declinePr(args.repo_slug as string, args.pr_id as number);
299
+ return {
300
+ pr_id: args.pr_id,
301
+ state: result.state,
302
+ };
303
+ },
304
+
305
+ get_pr_diff: async (args) => {
306
+ const client = getClient();
307
+ const diff = await client.getPrDiff(args.repo_slug as string, args.pr_id as number);
308
+ if (!diff) {
309
+ return { error: `PR #${args.pr_id} not found or has no diff` };
310
+ }
311
+ const maxLength = 50000;
312
+ const truncated = diff.length > maxLength;
313
+ return {
314
+ pr_id: args.pr_id,
315
+ diff: truncated ? diff.substring(0, maxLength) : diff,
316
+ truncated,
317
+ total_length: diff.length,
318
+ };
319
+ },
320
+ };
321
+