@stubbedev/atlassian-mcp 0.1.18 → 0.2.1

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.
package/dist/index.js CHANGED
@@ -1,16 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  import 'dotenv/config';
3
+ import { execSync } from 'child_process';
3
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
6
  import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode, } from '@modelcontextprotocol/sdk/types.js';
6
7
  import { loadConfig } from './config.js';
7
8
  import { JiraClient } from './jira.js';
8
- import { BitbucketClient } from './bitbucket.js';
9
- import { getContext, getCommits, getDiff } from './git.js';
9
+ import { BitbucketClient, parseBitbucketRemote } from './bitbucket.js';
10
+ import { getContext, getDiff, createBranch, checkRemoteBranch, checkoutRemoteBranch } from './git.js';
10
11
  import { getDevContext } from './context.js';
12
+ function currentGitRemote() {
13
+ try {
14
+ return execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
15
+ }
16
+ catch {
17
+ return '';
18
+ }
19
+ }
20
+ function remoteMatchesBitbucketInstance(remote, bitbucketUrl) {
21
+ if (!remote)
22
+ return false;
23
+ try {
24
+ const host = new URL(bitbucketUrl).hostname.toLowerCase();
25
+ return remote.toLowerCase().includes(host);
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
11
31
  const config = loadConfig();
12
- const jira = new JiraClient(config.jira.url, config.jira.token);
13
- const bitbucket = new BitbucketClient(config.bitbucket.url, config.bitbucket.token);
32
+ const jira = config.jira ? new JiraClient(config.jira.url, config.jira.token) : null;
33
+ const _remote = currentGitRemote();
34
+ const bitbucket = (config.bitbucket && remoteMatchesBitbucketInstance(_remote, config.bitbucket.url)) ? new BitbucketClient(config.bitbucket.url, config.bitbucket.token) : null;
35
+ if (config.bitbucket && !bitbucket) {
36
+ console.error(`[atlassian-mcp] Bitbucket configured but remote "${_remote || '(none)'}" does not match ${config.bitbucket.url} — Bitbucket tools disabled for this repo.`);
37
+ }
14
38
  const server = new Server({ name: 'atlassian-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
15
39
  server.onerror = (error) => console.error('[MCP Error]', error);
16
40
  function normalizeBitbucketArgs(args) {
@@ -40,792 +64,658 @@ function normalizeJiraMutateArgs(args) {
40
64
  return out;
41
65
  }
42
66
  const JIRA_WIKI_MARKUP_HINT = 'Use Jira wiki markup (Atlassian renderer syntax), not GitHub/CommonMark markdown.';
67
+ function issueTypePrefix(issueType) {
68
+ const t = issueType.toLowerCase();
69
+ if (t === 'bug' || t === 'bugfix' || t === 'defect')
70
+ return 'bugfix';
71
+ if (t === 'hotfix')
72
+ return 'hotfix';
73
+ if (t === 'task' || t === 'sub-task' || t === 'subtask')
74
+ return 'task';
75
+ return 'feature'; // story, feature, epic, improvement, etc.
76
+ }
77
+ function slugifyBranchName(issueKey, summary, issueType) {
78
+ const prefix = issueTypePrefix(issueType);
79
+ const slug = summary
80
+ .toLowerCase()
81
+ .replace(/[^a-z0-9]+/g, '-')
82
+ .replace(/^-|-$/g, '')
83
+ .slice(0, 40)
84
+ .replace(/-$/, '');
85
+ return `${prefix}/${issueKey}-${slug}`;
86
+ }
43
87
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
44
88
  tools: [
45
- // ── Context ───────────────────────────────────────────────────────────
46
- {
47
- name: 'get_dev_context',
48
- description: 'Use when you want one quick snapshot before coding or reviewing: current git branch/status, Jira tickets detected from branch name, and the open Bitbucket PR for that branch. For review requests, use this first to resolve the branch and PR without assuming it is in your personal inbox.',
49
- inputSchema: {
50
- type: 'object',
51
- properties: {
52
- repoPath: { type: 'string', description: 'Local path to the git repo (defaults to current working directory)' },
53
- },
54
- },
55
- },
56
- // ── Jira ──────────────────────────────────────────────────────────────
57
- {
58
- name: 'jira_search_issues',
59
- description: 'Use when you want to find Jira tickets by natural language, keywords, or filters. Supports plain text search and advanced JQL.',
60
- inputSchema: {
61
- type: 'object',
62
- properties: {
63
- query: { type: 'string', description: 'Plain-text ticket search across summary, description, and comments' },
64
- jql: { type: 'string', description: 'Raw JQL query (overrides other filters when provided)' },
65
- project: { type: 'string', description: 'Project key filter, for example "FOO"' },
66
- status: { type: 'string', description: 'Status filter, for example "In Progress"' },
67
- assignee: { type: 'string', description: 'Assignee username filter' },
68
- issueType: { type: 'string', description: 'Issue type filter, for example "Bug" or "Story"' },
69
- maxResults: { type: 'number', description: 'Max results per page (default 20)', default: 20 },
70
- startAt: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
71
- },
72
- },
73
- },
74
- {
75
- name: 'jira_my_issues',
76
- description: 'Use when you want your Jira work queue: tickets assigned to you, newest updates first.',
77
- inputSchema: {
78
- type: 'object',
79
- properties: {
80
- maxResults: { type: 'number', description: 'Max results per page (default 20)', default: 20 },
81
- startAt: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
82
- },
83
- },
84
- },
85
- {
86
- name: 'jira_get_projects',
87
- description: 'Use when you need available Jira projects and project codes before creating or searching tickets.',
88
- inputSchema: {
89
- type: 'object',
90
- properties: {
91
- maxResults: { type: 'number', description: 'Max results (default 50)', default: 50 },
92
- },
93
- },
94
- },
89
+ // ── Git (always available) ────────────────────────────────────────────
95
90
  {
96
- name: 'jira_get_issue_types',
97
- description: 'Use when preparing to create tickets and you need valid issue types and statuses. If projectKey/project is omitted, the server auto-picks from branch context or asks you to choose.',
98
- inputSchema: {
99
- type: 'object',
100
- properties: {
101
- projectKey: { type: 'string', description: 'Jira project code, e.g. "PAY" from PAY-123 (optional)' },
102
- project: { type: 'string', description: 'Alias for projectKey' },
103
- },
104
- },
105
- },
106
- {
107
- name: 'jira_get_sprints',
108
- description: 'Use when you need sprint IDs for planning or assignment. Returns sprints for a Jira board.',
109
- inputSchema: {
110
- type: 'object',
111
- properties: {
112
- boardId: { type: 'number', description: 'Jira board ID' },
113
- state: { type: 'string', description: 'Optional sprint state filter, e.g. "active", "future", or "closed"' },
114
- maxResults: { type: 'number', description: 'Max sprints per page (default 20)', default: 20 },
115
- startAt: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
116
- },
117
- required: ['boardId'],
118
- },
119
- },
120
- {
121
- name: 'jira_search_users',
122
- description: 'Use when assigning tickets and you need to find the correct Jira username by name or email.',
123
- inputSchema: {
124
- type: 'object',
125
- properties: {
126
- query: { type: 'string', description: 'Name or username to search for' },
127
- maxResults: { type: 'number', description: 'Max results (default 10)', default: 10 },
128
- },
129
- required: ['query'],
130
- },
131
- },
132
- {
133
- name: 'jira_get_issue',
134
- description: 'Use when you want full details for a specific Jira ticket by key (for example FOO-123).',
135
- inputSchema: {
136
- type: 'object',
137
- properties: {
138
- issueKey: { type: 'string', description: 'Jira issue key, e.g. "FOO-123"' },
139
- },
140
- required: ['issueKey'],
141
- },
142
- },
143
- {
144
- name: 'jira_issue_overview',
145
- description: 'Use when you want one Jira issue snapshot in a single call: details, transitions, sprint context, and optional comments.',
91
+ name: 'git_get_context',
92
+ description: 'Start here for any coding or review task: current branch, upstream ahead/behind, remote URL, recent commits, working tree status, diff stat summary, and Jira keys detected in the branch name. Pass includeDiff=true to also include the full uncommitted diff.',
146
93
  inputSchema: {
147
94
  type: 'object',
148
95
  properties: {
149
- issueKey: { type: 'string', description: 'Jira issue key, e.g. "FOO-123"' },
150
- includeComments: { type: 'boolean', description: 'Include comments in the overview (default true)', default: true },
151
- commentsMaxResults: { type: 'number', description: 'Max comments when includeComments=true (default 10)', default: 10 },
152
- commentsStartAt: { type: 'number', description: 'Comment pagination offset (default 0)', default: 0 },
153
- includeTransitions: { type: 'boolean', description: 'Include available transitions (default true)', default: true },
154
- includeSprint: { type: 'boolean', description: 'Include sprint data via Jira Agile API (default true)', default: true },
96
+ repoPath: { type: 'string', description: 'Path to the git repository (defaults to cwd)' },
97
+ commitLimit: { type: 'number', description: 'Number of recent commits to show (default 10)', default: 10 },
98
+ includeDiff: { type: 'boolean', description: 'Include full uncommitted diff (default false)', default: false },
155
99
  },
156
- required: ['issueKey'],
157
100
  },
158
101
  },
159
102
  {
160
- name: 'jira_create_issue',
161
- description: `Use when you want to create a new Jira ticket (bug, story, task, etc.). If projectKey/project is omitted, the server auto-picks from branch context or asks you to choose. ${JIRA_WIKI_MARKUP_HINT}`,
103
+ name: 'git_get_diff',
104
+ description: 'Get a diff between two git refs or commits. Use when you need to compare a feature branch to main, inspect a specific commit range, or review changes between two refs. For large diffs, increase maxChars or use charOffset to page through them.',
162
105
  inputSchema: {
163
106
  type: 'object',
164
107
  properties: {
165
- projectKey: { type: 'string', description: 'Jira project code, e.g. "PAY" from PAY-123 (optional)' },
166
- project: { type: 'string', description: 'Alias for projectKey' },
167
- issueType: { type: 'string', description: 'Issue type name, for example "Bug", "Story", or "Task"' },
168
- summary: { type: 'string', description: 'Issue title' },
169
- description: { type: 'string', description: `Issue description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
170
- assignee: { type: 'string', description: 'Username to assign to (optional)' },
171
- priority: { type: 'string', description: 'Priority name, e.g. "High" (optional)' },
172
- sprintId: { type: 'number', description: 'Sprint ID to immediately add the new issue into (optional)' },
108
+ repoPath: { type: 'string', description: 'Path to the git repository (defaults to cwd)' },
109
+ fromRef: { type: 'string', description: 'Base ref or commit' },
110
+ toRef: { type: 'string', description: 'Target ref or commit (requires fromRef)' },
111
+ maxChars: { type: 'number', description: 'Max characters to return (default 8000). Increase for large diffs.', default: 8000 },
112
+ charOffset: { type: 'number', description: 'Skip this many characters from the start (for paging large diffs)', default: 0 },
173
113
  },
174
- required: ['issueType', 'summary'],
175
114
  },
176
115
  },
177
- {
178
- name: 'jira_update_issue',
179
- description: `Use when you want to edit an existing Jira ticket: title, description, assignee, or priority. ${JIRA_WIKI_MARKUP_HINT}`,
180
- inputSchema: {
181
- type: 'object',
182
- properties: {
183
- issueKey: { type: 'string', description: 'Jira issue key' },
184
- summary: { type: 'string', description: 'New summary (optional)' },
185
- description: { type: 'string', description: `New description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
186
- assignee: { type: 'string', description: 'New assignee username, or empty string to unassign (optional)' },
187
- priority: { type: 'string', description: 'New priority name (optional)' },
188
- sprintId: { type: 'number', description: 'Sprint ID to add this issue into (optional)' },
116
+ // ── Combined context (jira + bitbucket, or either alone) ─────────────
117
+ ...(jira || bitbucket ? [{
118
+ name: 'get_dev_context',
119
+ description: 'Master entry point. Use when asked "what am I working on?", "what\'s the status?", "show me the context", or before any review or coding task. Returns: git branch + upstream state, Jira ticket overview (status, transitions, sprint, comments), open PR with reviewer approvals, and actionable next-step hints (create PR, merge, address blockers).',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: {
123
+ repoPath: { type: 'string', description: 'Local path to the git repo (defaults to cwd)' },
124
+ },
189
125
  },
190
- required: ['issueKey'],
191
- },
192
- },
193
- {
194
- name: 'jira_add_issues_to_sprint',
195
- description: 'Use when you want to assign one or more Jira issues to a sprint by sprint ID.',
196
- inputSchema: {
197
- type: 'object',
198
- properties: {
199
- sprintId: { type: 'number', description: 'Sprint ID' },
200
- issueKey: { type: 'string', description: 'Single issue key (optional)' },
201
- issueKeys: { type: 'array', items: { type: 'string' }, description: 'Multiple issue keys (optional)' },
126
+ }] : []),
127
+ // ── Jira ──────────────────────────────────────────────────────────────
128
+ ...(jira ? [
129
+ {
130
+ name: 'start_work',
131
+ description: 'Start working on a Jira ticket: fetches the ticket, creates a local git branch with an auto-generated name (e.g. feature/FOO-123-add-payment-gateway), and optionally transitions the ticket. Use when told "make a branch for FOO-123", "start working on this ticket", "check out a branch for this issue", or "begin work on FOO-123".',
132
+ inputSchema: {
133
+ type: 'object',
134
+ properties: {
135
+ issueKey: { type: 'string', description: 'Jira issue key, e.g. FOO-123' },
136
+ repoPath: { type: 'string', description: 'Local repo path (defaults to cwd)' },
137
+ baseBranch: { type: 'string', description: 'Branch to base off (default: master)' },
138
+ branchName: { type: 'string', description: 'Override the generated branch name' },
139
+ transitionName: { type: 'string', description: 'Jira transition to apply, e.g. "In Progress" (optional)' },
140
+ push: { type: 'boolean', description: 'Push branch to remote after creation (default false)', default: false },
141
+ },
142
+ required: ['issueKey'],
143
+ },
144
+ },
145
+ {
146
+ name: 'jira_search',
147
+ description: 'Discover Jira resources. Use when asked "find tickets for...", "what\'s in the backlog", "show me my issues", "list projects", or "which board is for project X". Set resource:\n• "issues" (default) — search by text, JQL, project, status, assignee, issue type, or mine=true for your queue\n• "projects" — list all projects and their keys\n• "issue_types" — valid types and statuses for a project\n• "boards" — list boards (pass project to filter by project key); use this to find the boardId before fetching sprints or board_overview\n• "sprints" — sprints for a board (pass boardId); if you don\'t know the boardId, first use resource=boards\n• "board_overview" — active/future sprints with their issues for a board (pass boardId); use when asked "what\'s in the sprint", "show me the board", or "what\'s everyone working on"\n• "users" — find users by name/email (pass query)',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ resource: { type: 'string', enum: ['issues', 'projects', 'issue_types', 'boards', 'sprints', 'board_overview', 'users'], description: 'What to search (default: issues)' },
152
+ mine: { type: 'boolean', description: 'Return issues assigned to you (resource=issues only)' },
153
+ query: { type: 'string', description: 'Text search or user name query' },
154
+ jql: { type: 'string', description: 'Raw JQL (resource=issues only, overrides other filters)' },
155
+ project: { type: 'string', description: 'Project key filter or scope for issue_types/boards' },
156
+ status: { type: 'string', description: 'Status filter (issues only, or board_overview to filter issues by status)' },
157
+ assignee: { type: 'string', description: 'Assignee username filter (issues only, or board_overview to filter issues by assignee)' },
158
+ issueType: { type: 'string', description: 'Issue type filter (issues only)' },
159
+ boardId: { type: 'number', description: 'Board ID (required for resource=sprints or board_overview)' },
160
+ sprintState: { type: 'string', description: 'Sprint state filter: active, future, closed (sprints and board_overview)' },
161
+ includeIssues: { type: 'boolean', description: 'Include issues per sprint in board_overview (default true)', default: true },
162
+ maxResults: { type: 'number', description: 'Max results (default 20)', default: 20 },
163
+ startAt: { type: 'number', description: 'Pagination offset (default 0)', default: 0 },
164
+ },
202
165
  },
203
- required: ['sprintId'],
204
166
  },
205
- },
206
- {
207
- name: 'jira_mutate_issue',
208
- description: `Use when you want to bundle Jira mutations in one call: create or target an issue, then optional update, sprint assignment, transition, and comment. ${JIRA_WIKI_MARKUP_HINT}`,
209
- inputSchema: {
210
- type: 'object',
211
- properties: {
212
- issueKey: { type: 'string', description: 'Existing issue key to mutate (optional if create is provided)' },
213
- create: {
214
- type: 'object',
215
- properties: {
216
- projectKey: { type: 'string', description: 'Jira project code (optional, auto-resolved when omitted)' },
217
- project: { type: 'string', description: 'Alias for projectKey' },
218
- issueType: { type: 'string', description: 'Issue type name, e.g. Bug, Story, Task' },
219
- summary: { type: 'string', description: 'Issue title' },
220
- description: { type: 'string', description: `Issue description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
221
- assignee: { type: 'string', description: 'Username to assign to (optional)' },
222
- priority: { type: 'string', description: 'Priority name (optional)' },
223
- },
224
- required: ['issueType', 'summary'],
167
+ {
168
+ name: 'jira_get',
169
+ description: 'Full details for one Jira issue: summary, description, status, assignee, sprint, available transitions, and recent comments. Use when asked to "show me FOO-123", "what does this ticket say", "get the details for this issue", or after discovering a key from get_dev_context or jira_search.',
170
+ inputSchema: {
171
+ type: 'object',
172
+ properties: {
173
+ issueKey: { type: 'string', description: 'Jira issue key, e.g. FOO-123' },
174
+ includeComments: { type: 'boolean', description: 'Include comments (default true)', default: true },
175
+ commentsMaxResults: { type: 'number', description: 'Max comments (default 10)', default: 10 },
176
+ commentsStartAt: { type: 'number', description: 'Comment pagination offset (default 0)', default: 0 },
177
+ includeTransitions: { type: 'boolean', description: 'Include available transitions (default true)', default: true },
178
+ includeSprint: { type: 'boolean', description: 'Include sprint data (default true)', default: true },
225
179
  },
226
- update: {
227
- type: 'object',
228
- properties: {
229
- summary: { type: 'string', description: 'New summary (optional)' },
230
- description: { type: 'string', description: `New description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
231
- assignee: { type: 'string', description: 'New assignee username, or empty string to unassign (optional)' },
232
- priority: { type: 'string', description: 'New priority name (optional)' },
180
+ required: ['issueKey'],
181
+ },
182
+ },
183
+ {
184
+ name: 'jira_mutate',
185
+ description: `Use when asked to "create a ticket", "log a bug", "move FOO-123 to In Progress", "close this issue", "assign to X", "add a comment on FOO-123", "FOO-123 blocks BAR-456", "log 2h on this ticket", or "add a sub-task". Bundles create/update/transition/comment/link/worklog in one call. ${JIRA_WIKI_MARKUP_HINT}`,
186
+ inputSchema: {
187
+ type: 'object',
188
+ properties: {
189
+ issueKey: { type: 'string', description: 'Existing issue key to mutate (optional if create is provided)' },
190
+ create: {
191
+ type: 'object',
192
+ properties: {
193
+ projectKey: { type: 'string', description: 'Jira project code (optional, auto-resolved when omitted)' },
194
+ project: { type: 'string', description: 'Alias for projectKey' },
195
+ issueType: { type: 'string', description: 'Issue type name, e.g. Bug, Story, Task, Sub-task' },
196
+ summary: { type: 'string', description: 'Issue title' },
197
+ description: { type: 'string', description: `Issue description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
198
+ assignee: { type: 'string', description: 'Username to assign to (optional)' },
199
+ priority: { type: 'string', description: 'Priority name (optional)' },
200
+ labels: { type: 'array', items: { type: 'string' }, description: 'Labels to apply (optional)' },
201
+ fixVersion: { type: 'string', description: 'Fix version name (optional)' },
202
+ parent: { type: 'string', description: 'Parent issue key for subtasks (optional)' },
203
+ },
204
+ required: ['issueType', 'summary'],
205
+ },
206
+ update: {
207
+ type: 'object',
208
+ properties: {
209
+ summary: { type: 'string', description: 'New summary (optional)' },
210
+ description: { type: 'string', description: `New description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
211
+ assignee: { type: 'string', description: 'New assignee username, or empty string to unassign (optional)' },
212
+ priority: { type: 'string', description: 'New priority name (optional)' },
213
+ labels: { type: 'array', items: { type: 'string' }, description: 'Replace label set (pass [] to clear)' },
214
+ fixVersion: { type: 'string', description: 'Fix version name, or empty string to clear (optional)' },
215
+ },
216
+ },
217
+ sprintId: { type: 'number', description: 'Sprint ID to add the issue into (optional)' },
218
+ removeFromSprint: { type: 'boolean', description: 'Move the issue to the backlog (remove from any sprint)' },
219
+ transitionId: { type: 'string', description: 'Transition ID (optional if transitionName provided)' },
220
+ transitionName: { type: 'string', description: 'Transition name, e.g. "In Progress" (optional if transitionId provided)' },
221
+ comment: { type: 'string', description: `Comment to add after other mutations (optional). ${JIRA_WIKI_MARKUP_HINT}` },
222
+ link: {
223
+ type: 'object',
224
+ description: 'Create an issue link, e.g. "FOO-123 blocks BAR-456"',
225
+ properties: {
226
+ linkType: { type: 'string', description: 'Link type name, e.g. "Blocks", "Relates to", "Duplicates"' },
227
+ targetIssueKey: { type: 'string', description: 'The other issue in the relationship' },
228
+ direction: { type: 'string', enum: ['outward', 'inward'], description: 'outward (default): issueKey → target; inward: target → issueKey' },
229
+ },
230
+ required: ['linkType', 'targetIssueKey'],
231
+ },
232
+ worklog: {
233
+ type: 'object',
234
+ description: 'Log time spent on this issue',
235
+ properties: {
236
+ timeSpent: { type: 'string', description: 'Time in Jira format, e.g. "2h 30m" or "1d"' },
237
+ comment: { type: 'string', description: 'Work description (optional)' },
238
+ started: { type: 'string', description: 'ISO 8601 datetime when work started (defaults to now)' },
239
+ },
240
+ required: ['timeSpent'],
233
241
  },
234
242
  },
235
- sprintId: { type: 'number', description: 'Sprint ID to add the issue into (optional)' },
236
- transitionId: { type: 'string', description: 'Transition ID (optional if transitionName is provided)' },
237
- transitionName: { type: 'string', description: 'Transition name, e.g. In Progress (optional if transitionId is provided)' },
238
- comment: { type: 'string', description: `Comment to add after other mutations (optional, no emoji). ${JIRA_WIKI_MARKUP_HINT}` },
239
- },
240
- },
241
- },
242
- {
243
- name: 'jira_board_overview',
244
- description: 'Use when you want one board-level planning snapshot: board info, sprints, and optional sprint issues in one call.',
245
- inputSchema: {
246
- type: 'object',
247
- properties: {
248
- boardId: { type: 'number', description: 'Jira board ID' },
249
- sprintState: { type: 'string', description: 'Sprint state filter, e.g. "active,future" (default active,future)' },
250
- sprintMaxResults: { type: 'number', description: 'Max sprints per page (default 10)', default: 10 },
251
- sprintStartAt: { type: 'number', description: 'Sprints pagination offset (default 0)', default: 0 },
252
- includeIssues: { type: 'boolean', description: 'Include sprint issues (default true)', default: true },
253
- issueMaxResults: { type: 'number', description: 'Max issues per sprint when includeIssues=true (default 25)', default: 25 },
254
- issueStartAt: { type: 'number', description: 'Issue pagination offset per sprint (default 0)', default: 0 },
255
- assignee: { type: 'string', description: 'Optional assignee filter for sprint issues' },
256
- status: { type: 'string', description: 'Optional status filter for sprint issues' },
257
- },
258
- required: ['boardId'],
259
- },
260
- },
261
- {
262
- name: 'jira_get_comments',
263
- description: 'Use when you want the discussion thread on a Jira ticket, with pagination for long threads. Includes comment IDs for follow-up edits.',
264
- inputSchema: {
265
- type: 'object',
266
- properties: {
267
- issueKey: { type: 'string', description: 'Jira issue key' },
268
- maxResults: { type: 'number', description: 'Max comments per page (default 50)', default: 50 },
269
- startAt: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
270
- },
271
- required: ['issueKey'],
272
- },
273
- },
274
- {
275
- name: 'jira_add_comment',
276
- description: `Use when you want to leave a comment on a Jira ticket. Keep comments concise and free of filler. Never include emojis. ${JIRA_WIKI_MARKUP_HINT}`,
277
- inputSchema: {
278
- type: 'object',
279
- properties: {
280
- issueKey: { type: 'string', description: 'Jira issue key' },
281
- body: { type: 'string', description: `Concise comment text. No filler. Do not include emojis. ${JIRA_WIKI_MARKUP_HINT}` },
282
- },
283
- required: ['issueKey', 'body'],
284
- },
285
- },
286
- {
287
- name: 'jira_edit_comment',
288
- description: `Use when you want to edit one of your own comments on a Jira ticket. Editing comments from other users is rejected. Never include emojis. ${JIRA_WIKI_MARKUP_HINT}`,
289
- inputSchema: {
290
- type: 'object',
291
- properties: {
292
- issueKey: { type: 'string', description: 'Jira issue key' },
293
- commentId: { type: 'string', description: 'Jira comment ID (from jira_get_comments or jira_issue_overview output)' },
294
- body: { type: 'string', description: `Updated concise comment text. No filler. Do not include emojis. ${JIRA_WIKI_MARKUP_HINT}` },
295
- },
296
- required: ['issueKey', 'commentId', 'body'],
297
- },
298
- },
299
- {
300
- name: 'jira_delete_comment',
301
- description: 'Use when you want to delete one of your own comments on a Jira ticket by comment ID. Deleting comments from other users is rejected.',
302
- inputSchema: {
303
- type: 'object',
304
- properties: {
305
- issueKey: { type: 'string', description: 'Jira issue key' },
306
- commentId: { type: 'string', description: 'Jira comment ID to delete (from jira_get_comments or jira_issue_overview output)' },
307
- },
308
- required: ['issueKey', 'commentId'],
309
- },
310
- },
311
- {
312
- name: 'jira_transition_issue',
313
- description: 'Use when you want to move a Jira ticket to another status. Provide a transition name (for example "In Progress") or a transition ID.',
314
- inputSchema: {
315
- type: 'object',
316
- properties: {
317
- issueKey: { type: 'string', description: 'Jira issue key, for example "FOO-123"' },
318
- transitionId: { type: 'string', description: 'Transition ID (optional if transitionName is provided)' },
319
- transitionName: { type: 'string', description: 'Transition name, for example "In Progress" (optional if transitionId is provided)' },
320
243
  },
321
- required: ['issueKey'],
322
244
  },
323
- },
324
- // ── Bitbucket ─────────────────────────────────────────────────────────
325
- {
326
- name: 'bitbucket_list_repos',
327
- description: 'Use when you want to browse repositories in Bitbucket, optionally scoped to a project code. You can pass projectKey or project.',
328
- inputSchema: {
329
- type: 'object',
330
- properties: {
331
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (optional)' },
332
- project: { type: 'string', description: 'Alias for projectKey' },
333
- limit: { type: 'number', description: 'Max repos per page (default 50)', default: 50 },
334
- start: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
335
- },
336
- },
337
- },
338
- {
339
- name: 'bitbucket_list_pull_requests',
340
- description: 'Use when you want pull requests for a repo (open, merged, or declined) with pagination. Primary review-discovery flow: resolve the branch, then filter by fromBranch to find the PR for that branch (do not assume it is your own PR). You can pass projectKey/repoSlug or project/repo.',
341
- inputSchema: {
342
- type: 'object',
343
- properties: {
344
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
345
- project: { type: 'string', description: 'Alias for projectKey' },
346
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
347
- repo: { type: 'string', description: 'Alias for repoSlug' },
348
- state: { type: 'string', enum: ['OPEN', 'MERGED', 'DECLINED'], description: 'PR state filter (default OPEN)', default: 'OPEN' },
349
- fromBranch: { type: 'string', description: 'Filter to PRs from this source branch (optional)' },
350
- text: { type: 'string', description: 'Filter PRs where title or description contains this text (optional)' },
351
- limit: { type: 'number', description: 'Max PRs per page (default 25)', default: 25 },
352
- start: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
353
- },
354
- },
355
- },
356
- {
357
- name: 'bitbucket_my_prs',
358
- description: 'Use only when you explicitly want your personal PR inbox (reviews requested, authored by you, or participated PRs). Do not use this for branch-based review targeting.',
359
- inputSchema: {
360
- type: 'object',
361
- properties: {
362
- limit: { type: 'number', description: 'Max PRs per page (default 25)', default: 25 },
363
- start: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
364
- role: { type: 'string', enum: ['author', 'reviewer', 'participant'], description: 'Inbox role filter (default server behavior)' },
365
- },
366
- },
367
- },
368
- {
369
- name: 'bitbucket_get_pull_request',
370
- description: 'Use when you want metadata for one PR: title, branches, author, reviewers, and description. You can pass projectKey/repoSlug or project/repo.',
371
- inputSchema: {
372
- type: 'object',
373
- properties: {
374
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
375
- project: { type: 'string', description: 'Alias for projectKey' },
376
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
377
- repo: { type: 'string', description: 'Alias for repoSlug' },
378
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
379
- },
380
- required: ['prId'],
381
- },
382
- },
383
- {
384
- name: 'bitbucket_get_pr_overview',
385
- description: 'Use when you want one bulk PR snapshot in a single call: metadata, commits, comments, task-style BLOCKER comments, and optional diff. If prId is unknown during review, discover it from branch context first (get_dev_context or bitbucket_list_pull_requests with fromBranch). For complete file context while reviewing, follow up with bitbucket_get_file at the PR source ref instead of relying on local checkout state.',
386
- inputSchema: {
387
- type: 'object',
388
- properties: {
389
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
390
- project: { type: 'string', description: 'Alias for projectKey' },
391
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
392
- repo: { type: 'string', description: 'Alias for repoSlug' },
393
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
394
- includeCommits: { type: 'boolean', description: 'Include commit list (default true)', default: true },
395
- includeComments: { type: 'boolean', description: 'Include review comments/blockers (default true)', default: true },
396
- includeDiff: { type: 'boolean', description: 'Include diff text (default false)', default: false },
397
- commentsState: { type: 'string', enum: ['OPEN', 'RESOLVED', 'PENDING'], description: 'Comment state filter (default OPEN)', default: 'OPEN' },
398
- commentsSeverity: { type: 'string', enum: ['ALL', 'NORMAL', 'BLOCKER'], description: 'Comment severity filter (default ALL). BLOCKER means task/checklist-style review comments.', default: 'ALL' },
399
- commentsLimit: { type: 'number', description: 'Max comments per page (default 50)', default: 50 },
400
- commentsStart: { type: 'number', description: 'Comment pagination offset (default 0)', default: 0 },
401
- commitsLimit: { type: 'number', description: 'Max commits per page (default 25)', default: 25 },
402
- commitsStart: { type: 'number', description: 'Commit pagination offset (default 0)', default: 0 },
403
- diffMaxChars: { type: 'number', description: 'Max diff characters when includeDiff=true (default 8000)', default: 8000 },
404
- },
405
- required: ['prId'],
406
- },
407
- },
408
- {
409
- name: 'bitbucket_get_pr_diff',
410
- description: 'Use when you want the code changes for one PR as a unified diff. During review, treat this as patch context only and fetch full files with bitbucket_get_file (using the PR source ref) before making final judgments. You can pass projectKey/repoSlug or project/repo.',
411
- inputSchema: {
412
- type: 'object',
413
- properties: {
414
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
415
- project: { type: 'string', description: 'Alias for projectKey' },
416
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
417
- repo: { type: 'string', description: 'Alias for repoSlug' },
418
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
419
- },
420
- required: ['prId'],
421
- },
422
- },
423
- {
424
- name: 'bitbucket_get_pr_commits',
425
- description: 'Use when you want commit history included in a PR. You can pass projectKey/repoSlug or project/repo.',
426
- inputSchema: {
427
- type: 'object',
428
- properties: {
429
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
430
- project: { type: 'string', description: 'Alias for projectKey' },
431
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
432
- repo: { type: 'string', description: 'Alias for repoSlug' },
433
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
434
- limit: { type: 'number', description: 'Max commits (default 25)', default: 25 },
435
- start: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
436
- },
437
- required: ['prId'],
438
- },
439
- },
440
- {
441
- name: 'bitbucket_create_pull_request',
442
- description: 'Use when you want to open a new PR. Project/repo auto-detect from git remote, source branch auto-detects from current branch if omitted, and this call first checks for an existing open PR from that source branch.',
443
- inputSchema: {
444
- type: 'object',
445
- properties: {
446
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
447
- project: { type: 'string', description: 'Alias for projectKey' },
448
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
449
- repo: { type: 'string', description: 'Alias for repoSlug' },
450
- title: { type: 'string', description: 'PR title' },
451
- description: { type: 'string', description: 'PR description (optional)' },
452
- fromBranch: { type: 'string', description: 'Source branch name (defaults to current branch)' },
453
- toBranch: { type: 'string', description: 'Target branch name (default: master)' },
454
- reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer usernames (optional)' },
245
+ {
246
+ name: 'jira_comment',
247
+ description: `Add, update, or delete a comment on a Jira issue. Use when asked to "edit my comment on FOO-123", "delete comment 12345", or "update that comment". action defaults to "add". Can only edit/delete your own comments. ${JIRA_WIKI_MARKUP_HINT}`,
248
+ inputSchema: {
249
+ type: 'object',
250
+ properties: {
251
+ action: { type: 'string', enum: ['add', 'update', 'delete'], description: 'Operation (default: add)' },
252
+ issueKey: { type: 'string', description: 'Jira issue key, e.g. FOO-123' },
253
+ commentId: { type: 'string', description: 'Comment ID (required for update/delete)' },
254
+ body: { type: 'string', description: `Comment text. ${JIRA_WIKI_MARKUP_HINT} Required for add/update.` },
255
+ },
256
+ required: ['issueKey'],
257
+ },
258
+ }
259
+ ] : []),
260
+ ...(bitbucket ? [{
261
+ name: 'bitbucket_search',
262
+ description: 'Discover Bitbucket resources. Use when asked "what PRs are open?", "show me the repos", "find the PR for this branch", or "list branches". Set resource:\n• "pull_requests" (default) list PRs by state/branch/text; mine=true for your inbox\n• "repos" list repositories in a project\n• "branches" — list or filter branches in a repo',
263
+ inputSchema: {
264
+ type: 'object',
265
+ properties: {
266
+ resource: { type: 'string', enum: ['pull_requests', 'repos', 'branches'], description: 'What to search (default: pull_requests)' },
267
+ mine: { type: 'boolean', description: 'Return your own PRs by role (resource=pull_requests only)' },
268
+ role: { type: 'string', enum: ['author', 'reviewer', 'participant'], description: 'Your role filter when mine=true' },
269
+ projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG"' },
270
+ project: { type: 'string', description: 'Alias for projectKey' },
271
+ repoSlug: { type: 'string', description: 'Repository slug' },
272
+ repo: { type: 'string', description: 'Alias for repoSlug' },
273
+ state: { type: 'string', enum: ['OPEN', 'MERGED', 'DECLINED'], description: 'PR state filter (default OPEN)' },
274
+ fromBranch: { type: 'string', description: 'Filter PRs from this source branch' },
275
+ text: { type: 'string', description: 'Filter PRs by title/description text' },
276
+ filter: { type: 'string', description: 'Branch name filter (resource=branches only)' },
277
+ limit: { type: 'number', description: 'Max results per page (default 25)', default: 25 },
278
+ start: { type: 'number', description: 'Pagination offset (default 0)', default: 0 },
279
+ },
455
280
  },
456
- required: ['title'],
457
281
  },
458
- },
459
- {
460
- name: 'bitbucket_update_pull_request',
461
- description: 'Use when you want to update an existing PR title, description, destination branch, or reviewers. You can pass projectKey/repoSlug or project/repo.',
462
- inputSchema: {
463
- type: 'object',
464
- properties: {
465
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
466
- project: { type: 'string', description: 'Alias for projectKey' },
467
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
468
- repo: { type: 'string', description: 'Alias for repoSlug' },
469
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
470
- title: { type: 'string', description: 'Updated PR title (optional)' },
471
- description: { type: 'string', description: 'Updated PR description, or empty string to clear (optional)' },
472
- toBranch: { type: 'string', description: 'Updated target branch name (optional)' },
473
- reviewers: { type: 'array', items: { type: 'string' }, description: 'Updated reviewer usernames (optional). Pass an empty array to clear reviewers.' },
282
+ {
283
+ name: 'bitbucket_get_pr',
284
+ description: 'Full details for one PR: metadata, commits, open comments, blockers, and optional diff. Use when asked to "review this PR", "show me the review comments", "what\'s blocking the merge", or after get_dev_context surfaces a prId. For full file context during review, follow up with bitbucket_get_file. The response includes a "Viewing as" line — if it says "you are the author", do NOT add review comments or a summary unless explicitly asked; just answer questions about the PR. If it says "you are a reviewer", default to posting inline comments for suggested changes and a final summary comment.',
285
+ inputSchema: {
286
+ type: 'object',
287
+ properties: {
288
+ projectKey: { type: 'string', description: 'Bitbucket project code (usually auto-detected)' },
289
+ project: { type: 'string', description: 'Alias for projectKey' },
290
+ repoSlug: { type: 'string', description: 'Repository slug (usually auto-detected)' },
291
+ repo: { type: 'string', description: 'Alias for repoSlug' },
292
+ prId: { type: 'number', description: 'Pull request number (optional if fromBranch provided or running from a checked-out branch)' },
293
+ fromBranch: { type: 'string', description: 'Source branch auto-resolves the open PR; omit to use current checked-out branch' },
294
+ includeCommits: { type: 'boolean', description: 'Include commit list (default true)', default: true },
295
+ includeComments: { type: 'boolean', description: 'Include review comments and blockers (default true)', default: true },
296
+ includeDiff: { type: 'boolean', description: 'Include diff text (default false)', default: false },
297
+ includeBuildStatus: { type: 'boolean', description: 'Include CI/build status for the head commit (default true)', default: true },
298
+ commentsState: { type: 'string', enum: ['OPEN', 'RESOLVED', 'PENDING'], description: 'Comment state filter (default OPEN)', default: 'OPEN' },
299
+ commentsSeverity: { type: 'string', enum: ['ALL', 'NORMAL', 'BLOCKER'], description: 'Comment severity filter (default ALL)', default: 'ALL' },
300
+ commentsLimit: { type: 'number', description: 'Max comments (default 50)', default: 50 },
301
+ commentsStart: { type: 'number', description: 'Comment pagination offset (default 0)', default: 0 },
302
+ commitsLimit: { type: 'number', description: 'Max commits (default 25)', default: 25 },
303
+ diffMaxChars: { type: 'number', description: 'Max diff chars when includeDiff=true (default 8000)', default: 8000 },
304
+ },
474
305
  },
475
- required: ['prId'],
476
306
  },
477
- },
478
- {
479
- name: 'bitbucket_mutate_pull_request',
480
- description: 'Use when you want one ergonomic PR mutation call: target a PR by prId, or auto-target the open PR from create.fromBranch/current branch, then optionally update it; if none exists and create is provided, create a new PR.',
481
- inputSchema: {
482
- type: 'object',
483
- properties: {
484
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
485
- project: { type: 'string', description: 'Alias for projectKey' },
486
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
487
- repo: { type: 'string', description: 'Alias for repoSlug' },
488
- prId: { type: 'number', description: 'Target pull request number (optional). If omitted, tool targets the open PR for create.fromBranch/current branch.' },
489
- create: {
490
- type: 'object',
491
- properties: {
492
- title: { type: 'string', description: 'PR title' },
493
- description: { type: 'string', description: 'PR description (optional)' },
494
- fromBranch: { type: 'string', description: 'Source branch to target/create from (defaults to current branch)' },
495
- toBranch: { type: 'string', description: 'Destination branch (default: master when creating)' },
496
- reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer usernames (optional)' },
307
+ {
308
+ name: 'bitbucket_mutate',
309
+ description: 'Use when asked to "open a PR for this branch", "create a pull request", "approve this PR", "merge it", "ship it", or "decline this PR". Auto-targets the open PR for the current branch when prId is omitted. Handles create, update, approve/unapprove, decline, and merge in one call.',
310
+ inputSchema: {
311
+ type: 'object',
312
+ properties: {
313
+ projectKey: { type: 'string', description: 'Bitbucket project code (usually auto-detected)' },
314
+ project: { type: 'string', description: 'Alias for projectKey' },
315
+ repoSlug: { type: 'string', description: 'Repository slug (usually auto-detected)' },
316
+ repo: { type: 'string', description: 'Alias for repoSlug' },
317
+ prId: { type: 'number', description: 'Target PR number (optional, auto-resolved from branch)' },
318
+ action: { type: 'string', enum: ['approve', 'unapprove', 'decline', 'merge'], description: 'Lifecycle action to perform (optional)' },
319
+ mergeStrategy: { type: 'string', enum: ['MERGE_COMMIT', 'SQUASH', 'FAST_FORWARD'], description: 'Merge strategy (action=merge only)' },
320
+ mergeMessage: { type: 'string', description: 'Custom merge commit message (action=merge only)' },
321
+ declineMessage: { type: 'string', description: 'Decline message (action=decline only)' },
322
+ create: {
323
+ type: 'object',
324
+ properties: {
325
+ title: { type: 'string', description: 'PR title' },
326
+ description: { type: 'string', description: 'PR description (optional)' },
327
+ fromBranch: { type: 'string', description: 'Source branch (defaults to current branch)' },
328
+ toBranch: { type: 'string', description: 'Target branch (default: master)' },
329
+ reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer usernames (optional)' },
330
+ },
331
+ required: ['title'],
497
332
  },
498
- required: ['title'],
499
- },
500
- update: {
501
- type: 'object',
502
- properties: {
503
- title: { type: 'string', description: 'Updated PR title (optional)' },
504
- description: { type: 'string', description: 'Updated PR description, or empty string to clear (optional)' },
505
- toBranch: { type: 'string', description: 'Updated target branch (optional)' },
506
- reviewers: { type: 'array', items: { type: 'string' }, description: 'Updated reviewer usernames (optional). Pass an empty array to clear reviewers.' },
333
+ update: {
334
+ type: 'object',
335
+ properties: {
336
+ title: { type: 'string', description: 'Updated PR title (optional)' },
337
+ description: { type: 'string', description: 'Updated description, or empty string to clear (optional)' },
338
+ toBranch: { type: 'string', description: 'Updated target branch (optional)' },
339
+ reviewers: { type: 'array', items: { type: 'string' }, description: 'Updated reviewer usernames. Empty array clears reviewers.' },
340
+ },
507
341
  },
508
342
  },
509
343
  },
510
344
  },
511
- },
512
- {
513
- name: 'bitbucket_approve_pr',
514
- description: 'Use when you want to approve a PR. You can pass projectKey/repoSlug or project/repo.',
515
- inputSchema: {
516
- type: 'object',
517
- properties: {
518
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
519
- project: { type: 'string', description: 'Alias for projectKey' },
520
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
521
- repo: { type: 'string', description: 'Alias for repoSlug' },
522
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
523
- },
524
- required: ['prId'],
525
- },
526
- },
527
- {
528
- name: 'bitbucket_unapprove_pr',
529
- description: 'Use when you want to remove your PR approval. You can pass projectKey/repoSlug or project/repo.',
530
- inputSchema: {
531
- type: 'object',
532
- properties: {
533
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
534
- project: { type: 'string', description: 'Alias for projectKey' },
535
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
536
- repo: { type: 'string', description: 'Alias for repoSlug' },
537
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
538
- },
539
- required: ['prId'],
540
- },
541
- },
542
- {
543
- name: 'bitbucket_decline_pr',
544
- description: 'Use when you want to decline/close a PR without merging. You can pass projectKey/repoSlug or project/repo.',
545
- inputSchema: {
546
- type: 'object',
547
- properties: {
548
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
549
- project: { type: 'string', description: 'Alias for projectKey' },
550
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
551
- repo: { type: 'string', description: 'Alias for repoSlug' },
552
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
553
- message: { type: 'string', description: 'Optional decline message' },
554
- },
555
- required: ['prId'],
556
- },
557
- },
558
- {
559
- name: 'bitbucket_merge_pr',
560
- description: 'Use when you want to merge/land/ship a PR. You can pass projectKey/repoSlug or project/repo.',
561
- inputSchema: {
562
- type: 'object',
563
- properties: {
564
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
565
- project: { type: 'string', description: 'Alias for projectKey' },
566
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
567
- repo: { type: 'string', description: 'Alias for repoSlug' },
568
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
569
- mergeStrategy: { type: 'string', enum: ['MERGE_COMMIT', 'SQUASH', 'FAST_FORWARD'], description: 'Merge strategy (uses repo default if omitted)' },
570
- message: { type: 'string', description: 'Custom merge commit message (optional)' },
571
- },
572
- required: ['prId'],
573
- },
574
- },
575
- {
576
- name: 'bitbucket_get_pr_comments',
577
- description: 'Use when you want PR review discussion in bulk: comment threads, task-style BLOCKER comments, and blocker counts with pagination. The returned comment IDs are used as commentId in bitbucket_add_pr_comment (reply mode), bitbucket_update_pr_comment, and bitbucket_delete_pr_comment. For review tasks, locate PR by branch first instead of assuming it is in your inbox. You can pass projectKey/repoSlug or project/repo.',
578
- inputSchema: {
579
- type: 'object',
580
- properties: {
581
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
582
- project: { type: 'string', description: 'Alias for projectKey' },
583
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
584
- repo: { type: 'string', description: 'Alias for repoSlug' },
585
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
586
- path: { type: 'string', description: 'Optional file path filter, e.g. "src/index.ts"' },
587
- state: { type: 'string', enum: ['OPEN', 'RESOLVED', 'PENDING'], description: 'State filter. For normal comments this maps to threadResolved (OPEN/RESOLVED). For BLOCKER tasks this uses task state (OPEN/RESOLVED; PENDING allowed only for non-BLOCKER).', default: 'OPEN' },
588
- severity: { type: 'string', enum: ['ALL', 'NORMAL', 'BLOCKER'], description: 'Comment severity filter. BLOCKER means task/checklist-style review comments.', default: 'ALL' },
589
- countOnly: { type: 'boolean', description: 'When true with severity=BLOCKER, returns counts instead of comment bodies', default: false },
590
- limit: { type: 'number', description: 'Max items per page (default 50)', default: 50 },
591
- start: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
592
- },
593
- required: ['prId'],
594
- },
595
- },
596
- {
597
- name: 'bitbucket_add_pr_comment',
598
- description: `Add a PR review comment or reply to an existing thread.
599
-
600
- REPLIES TO SPECIFIC COMMENTS ARE MANDATORY: If your remark is about an existing comment/thread, you MUST reply in that thread by setting commentId. Do not post a new top-level PR comment or a new inline thread for follow-up remarks on an existing comment.
601
-
602
- FOR REVIEW FEEDBACK, DEFAULT TO INLINE + EXAMPLE: Prefer anchored inline comments with a concrete code change example (suggestion) over top-level prose. Use top-level comments only for overall PR-wide feedback.
603
-
604
- INLINE COMMENTS ARE STRONGLY PREFERRED: Whenever your comment refers to a specific line or block of code, you MUST provide filePath and line to anchor it as an inline comment on the diff. General top-level comments (no filePath/line) should only be used for overall PR feedback that does not relate to any particular line.
605
-
606
- SUGGESTIONS ARE STRONGLY PREFERRED OVER PLAIN COMMENTS: When you are pointing out something that should be changed, always provide a suggestion (the corrected code) rather than describing the change in words. A suggestion lets the author apply the fix with one click. Only omit suggestion if you are asking a question or raising a concern that has no clear single answer.
607
-
608
- Keep comments concise, plain text, and free of filler. Never include emojis. You can pass projectKey/repoSlug or project/repo.`,
609
- inputSchema: {
610
- type: 'object',
611
- properties: {
612
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
613
- project: { type: 'string', description: 'Alias for projectKey' },
614
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
615
- repo: { type: 'string', description: 'Alias for repoSlug' },
616
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
617
- commentId: { type: 'number', description: 'Comment ID to reply to. Same identifier style as update/delete tools (optional).' },
618
- text: { type: 'string', description: 'Concise comment text. No filler. Do not include emojis. If suggestion is also provided, this text appears above the suggestion block.' },
619
- filePath: { type: 'string', description: 'Destination file path for inline comment, e.g. "src/index.ts". Must be provided together with line.' },
620
- srcPath: { type: 'string', description: 'Source file path. Only needed when the file was renamed; otherwise omit (defaults to filePath).' },
621
- line: { type: 'number', description: 'Line number in the file to anchor the comment to. Must be provided together with filePath.' },
622
- lineType: { type: 'string', enum: ['ADDED', 'REMOVED', 'CONTEXT'], description: 'The type of the anchored line in the diff. Defaults to ADDED. Use CONTEXT for unchanged lines, REMOVED for deleted lines.' },
623
- fileType: { type: 'string', enum: ['TO', 'FROM'], description: 'Which side of the diff the anchor refers to: TO (destination/new file, default) or FROM (source/old file).' },
624
- multilineStartLine: { type: 'number', description: 'First line of a multiline anchor. Set together with line (last line) to span multiple lines.' },
625
- multilineStartLineType: { type: 'string', enum: ['ADDED', 'REMOVED', 'CONTEXT'], description: 'Line type for the multilineStartLine. Defaults to lineType.' },
626
- suggestion: { type: 'string', description: 'Replacement code to suggest. Rendered as a suggestion block the author can apply with one click. STRONGLY PREFERRED whenever you are proposing a code change. Must be used with filePath and line.' },
627
- },
628
- required: ['prId', 'text'],
629
- },
630
- },
631
- {
632
- name: 'bitbucket_update_pr_comment',
633
- description: 'Use when you want to edit your own PR comments, resolve/reopen normal discussion threads, or manage task-style BLOCKER comments. Editing comments from other users is rejected. Hint: for normal comments, resolve/reopen means threadResolved; for BLOCKER tasks, resolve/reopen uses state. Keep comments concise, plain text, and free of filler. Never include emojis. You can pass projectKey/repoSlug or project/repo.',
634
- inputSchema: {
635
- type: 'object',
636
- properties: {
637
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
638
- project: { type: 'string', description: 'Alias for projectKey' },
639
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
640
- repo: { type: 'string', description: 'Alias for repoSlug' },
641
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
642
- commentId: { type: 'number', description: 'Comment ID to update' },
643
- text: { type: 'string', description: 'New concise comment text only. No filler. Do not include emojis. (optional)' },
644
- state: { type: 'string', enum: ['OPEN', 'RESOLVED'], description: 'Task state for BLOCKER comments only (optional). Rejected for normal comments; use threadResolved instead.' },
645
- threadResolved: { type: 'boolean', description: 'Resolve/reopen normal comment threads in Bitbucket UI only (optional). Rejected for BLOCKER comments; use state instead.' },
646
- severity: { type: 'string', enum: ['NORMAL', 'BLOCKER'], description: 'Comment severity (optional). BLOCKER marks it as a task/checklist item.' },
647
- },
648
- required: ['prId', 'commentId'],
649
- },
650
- },
651
- {
652
- name: 'bitbucket_delete_pr_comment',
653
- description: 'Use when you want to delete one of your own PR comments by comment ID. Deleting comments from other users is rejected. You can pass projectKey/repoSlug or project/repo.',
654
- inputSchema: {
655
- type: 'object',
656
- properties: {
657
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
658
- project: { type: 'string', description: 'Alias for projectKey' },
659
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
660
- repo: { type: 'string', description: 'Alias for repoSlug' },
661
- prId: { type: 'number', description: 'Pull request number (PR ID)' },
662
- commentId: { type: 'number', description: 'Comment ID to delete' },
663
- },
664
- required: ['prId', 'commentId'],
665
- },
666
- },
667
- {
668
- name: 'bitbucket_get_branches',
669
- description: 'Use when you want to browse repository branches or find a branch by name. You can pass projectKey/repoSlug or project/repo.',
670
- inputSchema: {
671
- type: 'object',
672
- properties: {
673
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
674
- project: { type: 'string', description: 'Alias for projectKey' },
675
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
676
- repo: { type: 'string', description: 'Alias for repoSlug' },
677
- filter: { type: 'string', description: 'Filter branches by name (optional)' },
678
- limit: { type: 'number', description: 'Max branches per page (default 25)', default: 25 },
679
- start: { type: 'number', description: 'Offset for pagination (default 0)', default: 0 },
680
- },
681
- },
682
- },
683
- {
684
- name: 'bitbucket_get_file',
685
- description: 'Use when you want raw file content from Bitbucket at a branch, tag, or commit. In PR reviews, this is the preferred way to get full-file context so review quality does not depend on having the reviewed branch checked out locally. You can pass projectKey/repoSlug or project/repo.',
686
- inputSchema: {
687
- type: 'object',
688
- properties: {
689
- projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
690
- project: { type: 'string', description: 'Alias for projectKey' },
691
- repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
692
- repo: { type: 'string', description: 'Alias for repoSlug' },
693
- path: { type: 'string', description: 'File path, e.g. "src/index.ts"' },
694
- ref: { type: 'string', description: 'Branch, tag, or commit hash (defaults to default branch)' },
695
- },
696
- required: ['path'],
697
- },
698
- },
699
- // ── Git ───────────────────────────────────────────────────────────────
700
- {
701
- name: 'git_get_context',
702
- description: 'Use when you want a quick local git snapshot: branch, remote, recent commits, working tree status, and Jira keys in branch names.',
703
- inputSchema: {
704
- type: 'object',
705
- properties: {
706
- repoPath: { type: 'string', description: 'Path to the git repository (defaults to cwd)' },
707
- commitLimit: { type: 'number', description: 'Number of recent commits to show (default 10)', default: 10 },
708
- },
709
- },
710
- },
711
- {
712
- name: 'git_get_commits',
713
- description: 'Use when you want commit history for a branch with author, date, and message.',
714
- inputSchema: {
715
- type: 'object',
716
- properties: {
717
- repoPath: { type: 'string', description: 'Path to the git repository (defaults to cwd)' },
718
- limit: { type: 'number', description: 'Max commits to return (default 20)', default: 20 },
719
- branch: { type: 'string', description: 'Branch name to log (defaults to current branch)' },
720
- },
721
- },
722
- },
723
- {
724
- name: 'git_get_diff',
725
- description: 'Use when you want uncommitted changes or a diff between two refs/commits.',
726
- inputSchema: {
727
- type: 'object',
728
- properties: {
729
- repoPath: { type: 'string', description: 'Path to the git repository (defaults to cwd)' },
730
- fromRef: { type: 'string', description: 'Base ref or commit (optional)' },
731
- toRef: { type: 'string', description: 'Target ref or commit (optional, requires fromRef)' },
345
+ {
346
+ name: 'bitbucket_comment',
347
+ description: `Add, update, or delete a PR comment. action defaults to "add". For review feedback, prefer inline comments with a code suggestion. Replies MUST use commentId. Keep comments concise, no emojis. Only call proactively (without being asked) when you are a reviewer on the PR (i.e. "Viewing as" says "you are a reviewer") — never post unsolicited comments on PRs you authored.`,
348
+ inputSchema: {
349
+ type: 'object',
350
+ properties: {
351
+ action: { type: 'string', enum: ['add', 'update', 'delete'], description: 'Operation (default: add)' },
352
+ projectKey: { type: 'string', description: 'Bitbucket project code (usually auto-detected)' },
353
+ project: { type: 'string', description: 'Alias for projectKey' },
354
+ repoSlug: { type: 'string', description: 'Repository slug (usually auto-detected)' },
355
+ repo: { type: 'string', description: 'Alias for repoSlug' },
356
+ prId: { type: 'number', description: 'Pull request number' },
357
+ commentId: { type: 'number', description: 'Comment ID to reply to, update, or delete' },
358
+ text: { type: 'string', description: 'Comment text. No filler, no emojis. Required for add/update.' },
359
+ filePath: { type: 'string', description: 'File path for inline comment (must pair with line)' },
360
+ srcPath: { type: 'string', description: 'Source path if file was renamed (optional, defaults to filePath)' },
361
+ line: { type: 'number', description: 'Line number to anchor inline comment (must pair with filePath)' },
362
+ lineType: { type: 'string', enum: ['ADDED', 'REMOVED', 'CONTEXT'], description: 'Diff line type (default ADDED)' },
363
+ fileType: { type: 'string', enum: ['TO', 'FROM'], description: 'Diff side: TO (new, default) or FROM (old)' },
364
+ multilineStartLine: { type: 'number', description: 'First line of multiline anchor (pair with line as last line)' },
365
+ multilineStartLineType: { type: 'string', enum: ['ADDED', 'REMOVED', 'CONTEXT'], description: 'Line type for multilineStartLine' },
366
+ suggestion: { type: 'string', description: 'Replacement code to suggest (strongly preferred for code changes). Requires filePath + line.' },
367
+ state: { type: 'string', enum: ['OPEN', 'RESOLVED'], description: 'Task state for BLOCKER comments (update only)' },
368
+ threadResolved: { type: 'boolean', description: 'Resolve/reopen normal comment thread (update only)' },
369
+ severity: { type: 'string', enum: ['NORMAL', 'BLOCKER'], description: 'Comment severity. BLOCKER = checklist task.' },
370
+ },
371
+ required: ['prId'],
372
+ },
373
+ },
374
+ {
375
+ name: 'bitbucket_get_file',
376
+ description: 'Raw file content from Bitbucket at a branch, tag, or commit. Use during PR reviews to get full-file context without relying on local checkout.',
377
+ inputSchema: {
378
+ type: 'object',
379
+ properties: {
380
+ projectKey: { type: 'string', description: 'Bitbucket project code (usually auto-detected)' },
381
+ project: { type: 'string', description: 'Alias for projectKey' },
382
+ repoSlug: { type: 'string', description: 'Repository slug (usually auto-detected)' },
383
+ repo: { type: 'string', description: 'Alias for repoSlug' },
384
+ path: { type: 'string', description: 'File path, e.g. "src/index.ts"' },
385
+ ref: { type: 'string', description: 'Branch, tag, or commit hash (defaults to default branch)' },
386
+ },
387
+ required: ['path'],
388
+ },
389
+ },
390
+ {
391
+ name: 'bitbucket_pr_tasks',
392
+ description: 'Manage PR tasks (checklist items). Use when asked to "list the tasks on this PR", "create a task for FOO-123", "mark task #5 as done", or "add a checklist item". Tasks are distinct from comments — they appear as a checklist in the PR sidebar.',
393
+ inputSchema: {
394
+ type: 'object',
395
+ properties: {
396
+ action: { type: 'string', enum: ['list', 'create', 'resolve', 'reopen', 'delete'], description: 'Operation (default: list)' },
397
+ projectKey: { type: 'string', description: 'Bitbucket project code (usually auto-detected)' },
398
+ project: { type: 'string', description: 'Alias for projectKey' },
399
+ repoSlug: { type: 'string', description: 'Repository slug (usually auto-detected)' },
400
+ repo: { type: 'string', description: 'Alias for repoSlug' },
401
+ prId: { type: 'number', description: 'Pull request number' },
402
+ taskId: { type: 'number', description: 'Task ID (required for resolve/reopen/delete)' },
403
+ text: { type: 'string', description: 'Task description (required for create)' },
404
+ commentId: { type: 'number', description: 'Anchor the task to a specific comment ID (optional for create)' },
405
+ },
406
+ required: ['prId'],
407
+ },
408
+ }] : []),
409
+ // ── Combined workflow ─────────────────────────────────────────────────
410
+ ...(jira && bitbucket ? [{
411
+ name: 'complete_work',
412
+ description: 'Close the loop on a finished branch: merges the open PR and transitions the Jira ticket to Done (or a named transition). Use when asked to "ship this", "close out FOO-123", "merge and close the ticket", or "done with this branch". Mirrors start_work.',
413
+ inputSchema: {
414
+ type: 'object',
415
+ properties: {
416
+ issueKey: { type: 'string', description: 'Jira issue key to transition (auto-detected from branch name if omitted)' },
417
+ prId: { type: 'number', description: 'PR to merge (auto-detected from current branch if omitted)' },
418
+ repoPath: { type: 'string', description: 'Local repo path (defaults to cwd)' },
419
+ transitionName: { type: 'string', description: 'Jira transition to apply after merge (default: "Done")' },
420
+ mergeStrategy: { type: 'string', enum: ['MERGE_COMMIT', 'SQUASH', 'FAST_FORWARD'], description: 'Merge strategy (optional)' },
421
+ mergeMessage: { type: 'string', description: 'Custom merge commit message (optional)' },
422
+ projectKey: { type: 'string', description: 'Bitbucket project code (usually auto-detected)' },
423
+ repoSlug: { type: 'string', description: 'Repository slug (usually auto-detected)' },
424
+ skipJiraTransition: { type: 'boolean', description: 'Skip transitioning the Jira ticket (default false)' },
425
+ },
732
426
  },
733
- },
734
- },
427
+ }] : [])
735
428
  ],
736
429
  }));
737
430
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
738
431
  const { name, arguments: args = {} } = request.params;
739
432
  try {
740
433
  switch (name) {
741
- // Context
434
+ // Git
435
+ case 'git_get_context':
436
+ return getContext(args);
437
+ case 'git_get_diff': {
438
+ const diffArgs = args;
439
+ const result = getDiff(diffArgs);
440
+ const raw = result.content[0].text;
441
+ const offset = diffArgs.charOffset ?? 0;
442
+ const limit = diffArgs.maxChars ?? 8000;
443
+ if (offset === 0 && raw.length <= limit)
444
+ return result;
445
+ const chunk = raw.slice(offset, offset + limit);
446
+ const remaining = raw.length - offset - chunk.length;
447
+ const suffix = remaining > 0 ? `\n\n... (${remaining} more chars, use charOffset=${offset + chunk.length})` : '';
448
+ return { content: [{ type: 'text', text: chunk + suffix }] };
449
+ }
450
+ // Combined context + workflow
742
451
  case 'get_dev_context':
452
+ if (!jira && !bitbucket)
453
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
743
454
  return await getDevContext(args, jira, bitbucket);
455
+ case 'start_work': {
456
+ if (!jira)
457
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
458
+ const a = args;
459
+ const fields = await jira.getIssueFields(a.issueKey);
460
+ const branchName = a.branchName ?? slugifyBranchName(a.issueKey, fields.summary, fields.type);
461
+ const repoPath = a.repoPath ?? process.cwd();
462
+ // Check if branch already exists on remote before creating
463
+ const remote = checkRemoteBranch(branchName, repoPath);
464
+ if (remote.exists) {
465
+ const authorLine = remote.author ? `Last author: ${remote.author}` : null;
466
+ const commitLine = remote.date
467
+ ? `Last commit: ${remote.date} — ${remote.message ?? ''}${remote.sha ? ` (${remote.sha})` : ''}`
468
+ : null;
469
+ const contextLines = [authorLine, commitLine].filter(Boolean).join('\n');
470
+ const message = [
471
+ `Branch "${branchName}" already exists on remote.`,
472
+ `Ticket: ${a.issueKey} — ${fields.summary}`,
473
+ contextLines,
474
+ ].filter(Boolean).join('\n');
475
+ try {
476
+ const result = await server.elicitInput({
477
+ message,
478
+ requestedSchema: {
479
+ type: 'object',
480
+ properties: {
481
+ action: {
482
+ type: 'string',
483
+ title: 'What would you like to do?',
484
+ oneOf: [
485
+ { const: 'checkout', title: `Check out existing branch "${branchName}"` },
486
+ { const: 'new_name', title: 'Use a different branch name (re-run start_work with branchName)' },
487
+ { const: 'cancel', title: 'Cancel' },
488
+ ],
489
+ },
490
+ },
491
+ required: ['action'],
492
+ },
493
+ });
494
+ if (result.action === 'cancel' || result.action === 'decline') {
495
+ return { content: [{ type: 'text', text: 'Cancelled.' }] };
496
+ }
497
+ if (result.action === 'accept' && result.content?.action === 'checkout') {
498
+ const checkout = checkoutRemoteBranch(branchName, repoPath);
499
+ return { content: [{ type: 'text', text: `${message}\n\n${checkout.content[0].text}` }] };
500
+ }
501
+ // new_name — instruct the model to re-run with a custom name
502
+ return {
503
+ content: [{
504
+ type: 'text',
505
+ text: `${message}\n\nRe-run start_work with a custom branchName to proceed.`,
506
+ }],
507
+ };
508
+ }
509
+ catch {
510
+ // Client doesn't support elicitation — fall back to informational text
511
+ return {
512
+ content: [{
513
+ type: 'text',
514
+ text: [
515
+ message,
516
+ '',
517
+ 'Options:',
518
+ ` • Check out existing: git checkout --track origin/${branchName}`,
519
+ ` • Use a different name: re-run start_work with branchName set`,
520
+ ].join('\n'),
521
+ }],
522
+ };
523
+ }
524
+ }
525
+ const branchResult = createBranch({
526
+ branchName,
527
+ baseBranch: a.baseBranch,
528
+ repoPath,
529
+ push: a.push ?? false,
530
+ });
531
+ const lines = [
532
+ `Ticket: ${a.issueKey} — ${fields.summary}`,
533
+ `Status: ${fields.status}`,
534
+ branchResult.content[0].text,
535
+ ];
536
+ if (a.transitionName) {
537
+ try {
538
+ await jira.mutateIssue({ issueKey: a.issueKey, transitionName: a.transitionName });
539
+ lines.push(`Jira: transitioned → ${a.transitionName}`);
540
+ }
541
+ catch (err) {
542
+ lines.push(`Jira: could not transition — ${err.message}`);
543
+ }
544
+ }
545
+ if (bitbucket)
546
+ lines.push(``, `Next: push commits then use bitbucket_mutate to open a PR.`);
547
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
548
+ }
744
549
  // Jira
745
- case 'jira_search_issues':
746
- return await jira.searchIssues(args);
747
- case 'jira_my_issues':
748
- return await jira.myIssues(args);
749
- case 'jira_get_projects':
750
- return await jira.getProjects(args);
751
- case 'jira_get_issue_types':
752
- return await jira.getIssueTypes(normalizeJiraProjectArgs(args));
753
- case 'jira_get_sprints':
754
- return await jira.getSprints(args);
755
- case 'jira_search_users':
756
- return await jira.searchUsers(args);
757
- case 'jira_get_issue':
758
- return await jira.getIssue(args);
759
- case 'jira_issue_overview':
550
+ case 'jira_search': {
551
+ if (!jira)
552
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
553
+ const a = args;
554
+ const resource = a.resource ?? 'issues';
555
+ if (resource === 'projects')
556
+ return await jira.getProjects({ maxResults: a.maxResults });
557
+ if (resource === 'issue_types')
558
+ return await jira.getIssueTypes({ projectKey: a.projectKey ?? a.project });
559
+ if (resource === 'boards')
560
+ return await jira.getBoards({ projectKey: a.projectKey ?? a.project, maxResults: a.maxResults, startAt: a.startAt });
561
+ if (resource === 'sprints')
562
+ return await jira.getSprints({ boardId: a.boardId, state: a.sprintState, maxResults: a.maxResults, startAt: a.startAt });
563
+ if (resource === 'board_overview')
564
+ return await jira.boardOverview({ boardId: a.boardId, sprintState: a.sprintState, sprintMaxResults: a.maxResults, sprintStartAt: a.startAt, includeIssues: a.includeIssues, assignee: a.assignee, status: a.status });
565
+ if (resource === 'users')
566
+ return await jira.searchUsers({ query: a.query ?? '', maxResults: a.maxResults });
567
+ // issues (default)
568
+ if (a.mine)
569
+ return await jira.myIssues({ maxResults: a.maxResults, startAt: a.startAt });
570
+ return await jira.searchIssues(a);
571
+ }
572
+ case 'jira_get':
573
+ if (!jira)
574
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
760
575
  return await jira.issueOverview(args);
761
- case 'jira_create_issue':
762
- return await jira.createIssue(normalizeJiraProjectArgs(args));
763
- case 'jira_update_issue':
764
- return await jira.updateIssue(args);
765
- case 'jira_add_issues_to_sprint':
766
- return await jira.addIssuesToSprint(args);
767
- case 'jira_mutate_issue':
576
+ case 'jira_mutate':
577
+ if (!jira)
578
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
768
579
  return await jira.mutateIssue(normalizeJiraMutateArgs(args));
769
- case 'jira_board_overview':
770
- return await jira.boardOverview(args);
771
- case 'jira_get_comments':
772
- return await jira.getComments(args);
773
- case 'jira_add_comment':
774
- return await jira.addComment(args);
775
- case 'jira_edit_comment':
776
- return await jira.editComment(args);
777
- case 'jira_delete_comment':
778
- return await jira.deleteComment(args);
779
- case 'jira_transition_issue':
780
- return await jira.transitionIssue(args);
580
+ case 'jira_comment': {
581
+ if (!jira)
582
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
583
+ const a = normalizeJiraProjectArgs(args);
584
+ const action = a.action ?? 'add';
585
+ if (action === 'update')
586
+ return await jira.editComment({ issueKey: a.issueKey, commentId: a.commentId, body: a.body });
587
+ if (action === 'delete')
588
+ return await jira.deleteComment({ issueKey: a.issueKey, commentId: a.commentId });
589
+ return await jira.addComment({ issueKey: a.issueKey, body: a.body });
590
+ }
781
591
  // Bitbucket
782
- case 'bitbucket_list_repos':
783
- return await bitbucket.listRepos(normalizeBitbucketArgs(args));
784
- case 'bitbucket_list_pull_requests':
785
- return await bitbucket.listPullRequests(normalizeBitbucketArgs(args));
786
- case 'bitbucket_my_prs':
787
- return await bitbucket.myPrs(args);
788
- case 'bitbucket_get_pull_request':
789
- return await bitbucket.getPullRequest(normalizeBitbucketArgs(args));
790
- case 'bitbucket_get_pr_overview':
592
+ case 'bitbucket_search': {
593
+ if (!bitbucket)
594
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
595
+ const a = normalizeBitbucketArgs(args);
596
+ const resource = a.resource ?? 'pull_requests';
597
+ if (resource === 'repos')
598
+ return await bitbucket.listRepos(a);
599
+ if (resource === 'branches')
600
+ return await bitbucket.getBranches(a);
601
+ // pull_requests (default)
602
+ if (a.mine)
603
+ return await bitbucket.myPrs({ limit: a.limit, start: a.start, role: a.role });
604
+ return await bitbucket.listPullRequests(a);
605
+ }
606
+ case 'bitbucket_get_pr':
607
+ if (!bitbucket)
608
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
791
609
  return await bitbucket.getPrOverview(normalizeBitbucketArgs(args));
792
- case 'bitbucket_get_pr_diff':
793
- return await bitbucket.getPrDiff(normalizeBitbucketArgs(args));
794
- case 'bitbucket_get_pr_commits':
795
- return await bitbucket.getPrCommits(normalizeBitbucketArgs(args));
796
- case 'bitbucket_create_pull_request':
797
- return await bitbucket.createPullRequest(normalizeBitbucketArgs(args));
798
- case 'bitbucket_update_pull_request':
799
- return await bitbucket.updatePullRequest(normalizeBitbucketArgs(args));
800
- case 'bitbucket_mutate_pull_request':
801
- return await bitbucket.mutatePullRequest(normalizeBitbucketArgs(args));
802
- case 'bitbucket_approve_pr':
803
- return await bitbucket.approvePr(normalizeBitbucketArgs(args));
804
- case 'bitbucket_unapprove_pr':
805
- return await bitbucket.unapprovePr(normalizeBitbucketArgs(args));
806
- case 'bitbucket_decline_pr':
807
- return await bitbucket.declinePr(normalizeBitbucketArgs(args));
808
- case 'bitbucket_merge_pr':
809
- return await bitbucket.mergePr(normalizeBitbucketArgs(args));
810
- case 'bitbucket_get_pr_comments':
811
- return await bitbucket.getPrComments(normalizeBitbucketArgs(args));
812
- case 'bitbucket_add_pr_comment':
813
- return await bitbucket.addPrComment(normalizeBitbucketArgs(args));
814
- case 'bitbucket_update_pr_comment':
815
- return await bitbucket.updatePrComment(normalizeBitbucketArgs(args));
816
- case 'bitbucket_delete_pr_comment':
817
- return await bitbucket.deletePrComment(normalizeBitbucketArgs(args));
818
- case 'bitbucket_get_branches':
819
- return await bitbucket.getBranches(normalizeBitbucketArgs(args));
610
+ case 'bitbucket_mutate': {
611
+ if (!bitbucket)
612
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
613
+ const a = normalizeBitbucketArgs(args);
614
+ const action = a.action;
615
+ if (action === 'approve')
616
+ return await bitbucket.approvePr(a);
617
+ if (action === 'unapprove')
618
+ return await bitbucket.unapprovePr(a);
619
+ if (action === 'decline')
620
+ return await bitbucket.declinePr({ ...a, message: a.declineMessage });
621
+ if (action === 'merge')
622
+ return await bitbucket.mergePr({ ...a, message: a.mergeMessage, mergeStrategy: a.mergeStrategy });
623
+ return await bitbucket.mutatePullRequest(a);
624
+ }
625
+ case 'bitbucket_comment': {
626
+ if (!bitbucket)
627
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
628
+ const a = normalizeBitbucketArgs(args);
629
+ const action = a.action ?? 'add';
630
+ if (action === 'update')
631
+ return await bitbucket.updatePrComment(a);
632
+ if (action === 'delete')
633
+ return await bitbucket.deletePrComment(a);
634
+ return await bitbucket.addPrComment(a);
635
+ }
820
636
  case 'bitbucket_get_file':
637
+ if (!bitbucket)
638
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
821
639
  return await bitbucket.getFile(normalizeBitbucketArgs(args));
822
- // Git
823
- case 'git_get_context':
824
- return getContext(args);
825
- case 'git_get_commits':
826
- return getCommits(args);
827
- case 'git_get_diff':
828
- return getDiff(args);
640
+ case 'bitbucket_pr_tasks': {
641
+ if (!bitbucket)
642
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
643
+ const a = normalizeBitbucketArgs(args);
644
+ const action = (a.action ?? 'list');
645
+ if (action === 'list')
646
+ return await bitbucket.getPrTasks(a);
647
+ return await bitbucket.mutatePrTask({ ...a, action: action });
648
+ }
649
+ case 'complete_work': {
650
+ if (!jira || !bitbucket)
651
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
652
+ const a = args;
653
+ const repoPath = a.repoPath ?? process.cwd();
654
+ const lines = [];
655
+ // Resolve PR — by prId or current branch
656
+ let resolvedPrId = a.prId;
657
+ if (resolvedPrId === undefined) {
658
+ const { execSync } = await import('child_process');
659
+ const branch = (() => { try {
660
+ return execSync('git rev-parse --abbrev-ref HEAD', { cwd: repoPath, encoding: 'utf-8' }).trim();
661
+ }
662
+ catch {
663
+ return '';
664
+ } })();
665
+ if (!branch || branch === 'HEAD') {
666
+ throw new Error('Could not determine current branch. Provide prId or run from a checked-out branch.');
667
+ }
668
+ const remote = (() => { try {
669
+ return execSync('git remote get-url origin', { cwd: repoPath, encoding: 'utf-8' }).trim();
670
+ }
671
+ catch {
672
+ return '';
673
+ } })();
674
+ const parsed = parseBitbucketRemote(remote);
675
+ if (!parsed)
676
+ throw new Error('Could not parse Bitbucket remote URL. Provide projectKey/repoSlug explicitly.');
677
+ const projectKey = a.projectKey ?? parsed.projectKey;
678
+ const repoSlug = a.repoSlug ?? parsed.repoSlug;
679
+ const pr = await bitbucket.findOpenPrForBranch(projectKey, repoSlug, branch);
680
+ if (!pr)
681
+ throw new Error(`No open PR found for branch "${branch}". Provide prId explicitly.`);
682
+ resolvedPrId = pr.id;
683
+ lines.push(`Branch: ${branch} → PR #${resolvedPrId}`);
684
+ // Auto-detect Jira issue key from branch if not provided
685
+ if (!a.issueKey) {
686
+ const JIRA_KEY_RE = /\b([A-Z][A-Z0-9]+-\d+)\b/;
687
+ const match = branch.match(JIRA_KEY_RE);
688
+ if (match) {
689
+ a.issueKey = match[1];
690
+ lines.push(`Jira: auto-detected ${a.issueKey} from branch name`);
691
+ }
692
+ }
693
+ }
694
+ // Merge the PR
695
+ const mergeResult = await bitbucket.mergePr({
696
+ prId: resolvedPrId,
697
+ projectKey: a.projectKey,
698
+ repoSlug: a.repoSlug,
699
+ mergeStrategy: a.mergeStrategy,
700
+ message: a.mergeMessage,
701
+ });
702
+ lines.push(mergeResult.content[0].text);
703
+ // Transition Jira ticket
704
+ if (!a.skipJiraTransition && a.issueKey) {
705
+ const transitionName = a.transitionName ?? 'Done';
706
+ try {
707
+ await jira.mutateIssue({ issueKey: a.issueKey, transitionName });
708
+ lines.push(`Jira: ${a.issueKey} transitioned → ${transitionName}`);
709
+ }
710
+ catch (err) {
711
+ lines.push(`Jira: could not transition ${a.issueKey} — ${err.message}`);
712
+ }
713
+ }
714
+ else if (!a.skipJiraTransition) {
715
+ lines.push('Jira: no issue key — skipped transition (provide issueKey to transition)');
716
+ }
717
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
718
+ }
829
719
  default:
830
720
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
831
721
  }