@aaronsb/jira-cloud-mcp 0.2.0 → 0.2.3
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/README.md +1 -1
- package/build/client/jira-client.js +20 -16
- package/build/handlers/board-handlers.js +48 -21
- package/build/handlers/filter-handlers.js +33 -40
- package/build/handlers/issue-handlers.js +19 -19
- package/build/handlers/project-handlers.js +43 -30
- package/build/handlers/search-handlers.js +7 -7
- package/build/handlers/sprint-handlers.js +86 -40
- package/build/mcp/markdown-renderer.js +422 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -191,9 +191,9 @@ export class JiraClient {
|
|
|
191
191
|
// Remove escaped quotes from JQL
|
|
192
192
|
const cleanJql = jql.replace(/\\"/g, '"');
|
|
193
193
|
console.error(`Executing JQL search with query: ${cleanJql}`);
|
|
194
|
-
|
|
194
|
+
// Use the new enhanced search API (old /rest/api/3/search was deprecated Oct 2025)
|
|
195
|
+
const searchResults = await this.client.issueSearch.searchForIssuesUsingJqlEnhancedSearch({
|
|
195
196
|
jql: cleanJql,
|
|
196
|
-
startAt,
|
|
197
197
|
maxResults: Math.min(maxResults, 100),
|
|
198
198
|
fields: [
|
|
199
199
|
'summary',
|
|
@@ -203,39 +203,43 @@ export class JiraClient {
|
|
|
203
203
|
'status',
|
|
204
204
|
'resolution',
|
|
205
205
|
'duedate',
|
|
206
|
+
'parent',
|
|
206
207
|
this.customFields.startDate,
|
|
207
208
|
this.customFields.storyPoints,
|
|
208
209
|
'timeestimate',
|
|
209
210
|
'issuelinks',
|
|
210
211
|
],
|
|
211
|
-
expand: 'renderedFields'
|
|
212
|
+
expand: 'renderedFields',
|
|
212
213
|
});
|
|
213
214
|
const issues = (searchResults.issues || []).map(issue => ({
|
|
214
215
|
key: issue.key,
|
|
215
|
-
summary: issue.fields
|
|
216
|
+
summary: issue.fields?.summary,
|
|
216
217
|
description: issue.renderedFields?.description || '',
|
|
217
|
-
parent: issue.fields
|
|
218
|
-
assignee: issue.fields
|
|
219
|
-
reporter: issue.fields
|
|
220
|
-
status: issue.fields
|
|
221
|
-
resolution: issue.fields
|
|
222
|
-
dueDate: issue.fields
|
|
223
|
-
startDate: issue.fields[this.customFields.startDate] || null,
|
|
224
|
-
storyPoints: issue.fields[this.customFields.storyPoints] || null,
|
|
225
|
-
timeEstimate: issue.fields
|
|
226
|
-
issueLinks: (issue.fields
|
|
218
|
+
parent: issue.fields?.parent?.key || null,
|
|
219
|
+
assignee: issue.fields?.assignee?.displayName || null,
|
|
220
|
+
reporter: issue.fields?.reporter?.displayName || '',
|
|
221
|
+
status: issue.fields?.status?.name || '',
|
|
222
|
+
resolution: issue.fields?.resolution?.name || null,
|
|
223
|
+
dueDate: issue.fields?.duedate || null,
|
|
224
|
+
startDate: issue.fields?.[this.customFields.startDate] || null,
|
|
225
|
+
storyPoints: issue.fields?.[this.customFields.storyPoints] || null,
|
|
226
|
+
timeEstimate: issue.fields?.timeestimate || null,
|
|
227
|
+
issueLinks: (issue.fields?.issuelinks || []).map(link => ({
|
|
227
228
|
type: link.type?.name || '',
|
|
228
229
|
outward: link.outwardIssue?.key || null,
|
|
229
230
|
inward: link.inwardIssue?.key || null,
|
|
230
231
|
})),
|
|
231
232
|
}));
|
|
233
|
+
// Note: Enhanced search API uses token-based pagination, not offset-based
|
|
234
|
+
// The total count is not available in the new API
|
|
235
|
+
const hasMore = !!searchResults.nextPageToken;
|
|
232
236
|
return {
|
|
233
237
|
issues,
|
|
234
238
|
pagination: {
|
|
235
239
|
startAt,
|
|
236
240
|
maxResults,
|
|
237
|
-
total:
|
|
238
|
-
hasMore
|
|
241
|
+
total: hasMore ? issues.length + 1 : issues.length, // Approximate since total not available
|
|
242
|
+
hasMore,
|
|
239
243
|
}
|
|
240
244
|
};
|
|
241
245
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { MarkdownRenderer } from '../mcp/markdown-renderer.js';
|
|
3
3
|
// Helper function to normalize parameter names (support both snake_case and camelCase)
|
|
4
4
|
function normalizeArgs(args) {
|
|
5
5
|
const normalized = {};
|
|
@@ -108,7 +108,10 @@ async function handleGetBoard(jiraClient, args) {
|
|
|
108
108
|
}
|
|
109
109
|
// Convert to BoardData format
|
|
110
110
|
const boardData = {
|
|
111
|
-
|
|
111
|
+
id: board.id,
|
|
112
|
+
name: board.name,
|
|
113
|
+
type: board.type,
|
|
114
|
+
projectName: board.location?.projectName,
|
|
112
115
|
};
|
|
113
116
|
// Handle expansions
|
|
114
117
|
if (expansionOptions.sprints) {
|
|
@@ -123,13 +126,24 @@ async function handleGetBoard(jiraClient, args) {
|
|
|
123
126
|
// Continue even if sprints fail
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
|
-
//
|
|
127
|
-
const
|
|
129
|
+
// Render to markdown
|
|
130
|
+
const markdown = MarkdownRenderer.renderBoard({
|
|
131
|
+
id: boardData.id,
|
|
132
|
+
name: boardData.name,
|
|
133
|
+
type: boardData.type,
|
|
134
|
+
projectName: boardData.projectName,
|
|
135
|
+
sprints: boardData.sprints?.map((s) => ({
|
|
136
|
+
id: s.id,
|
|
137
|
+
name: s.name,
|
|
138
|
+
state: s.state,
|
|
139
|
+
goal: s.goal,
|
|
140
|
+
})),
|
|
141
|
+
});
|
|
128
142
|
return {
|
|
129
143
|
content: [
|
|
130
144
|
{
|
|
131
145
|
type: 'text',
|
|
132
|
-
text:
|
|
146
|
+
text: markdown,
|
|
133
147
|
},
|
|
134
148
|
],
|
|
135
149
|
};
|
|
@@ -145,7 +159,10 @@ async function handleListBoards(jiraClient, args) {
|
|
|
145
159
|
const paginatedBoards = boards.slice(startAt, startAt + maxResults);
|
|
146
160
|
// Convert to BoardData format
|
|
147
161
|
const boardDataList = paginatedBoards.map(board => ({
|
|
148
|
-
|
|
162
|
+
id: board.id,
|
|
163
|
+
name: board.name,
|
|
164
|
+
type: board.type,
|
|
165
|
+
projectName: board.location?.projectName,
|
|
149
166
|
}));
|
|
150
167
|
// If sprints are requested, get them for each board
|
|
151
168
|
if (includeSprints) {
|
|
@@ -163,25 +180,35 @@ async function handleListBoards(jiraClient, args) {
|
|
|
163
180
|
}
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
|
-
//
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
// Convert to markdown renderer format
|
|
184
|
+
const rendererBoards = boardDataList.map(board => ({
|
|
185
|
+
id: board.id,
|
|
186
|
+
name: board.name,
|
|
187
|
+
type: board.type,
|
|
188
|
+
projectName: board.projectName,
|
|
189
|
+
sprints: board.sprints?.map((s) => ({
|
|
190
|
+
id: s.id,
|
|
191
|
+
name: s.name,
|
|
192
|
+
state: s.state,
|
|
193
|
+
goal: s.goal,
|
|
194
|
+
})),
|
|
195
|
+
}));
|
|
196
|
+
// Render to markdown with pagination
|
|
197
|
+
let markdown = MarkdownRenderer.renderBoardList(rendererBoards);
|
|
198
|
+
// Add pagination guidance
|
|
199
|
+
markdown += '\n\n---\n';
|
|
200
|
+
if (startAt + maxResults < boards.length) {
|
|
201
|
+
markdown += `Showing ${startAt + 1}-${startAt + boardDataList.length} of ${boards.length}\n`;
|
|
202
|
+
markdown += `**Next page:** Use startAt=${startAt + maxResults}`;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
markdown += `Showing all ${boardDataList.length} board${boardDataList.length !== 1 ? 's' : ''}`;
|
|
206
|
+
}
|
|
180
207
|
return {
|
|
181
208
|
content: [
|
|
182
209
|
{
|
|
183
210
|
type: 'text',
|
|
184
|
-
text:
|
|
211
|
+
text: markdown,
|
|
185
212
|
},
|
|
186
213
|
],
|
|
187
214
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { MarkdownRenderer } from '../mcp/markdown-renderer.js';
|
|
3
3
|
// Helper function to normalize parameter names (support both snake_case and camelCase)
|
|
4
4
|
function normalizeArgs(args) {
|
|
5
5
|
const normalized = {};
|
|
@@ -134,14 +134,9 @@ async function handleGetFilter(jiraClient, args) {
|
|
|
134
134
|
name: filter.name || 'Unnamed Filter',
|
|
135
135
|
owner: filter.owner || 'Unknown',
|
|
136
136
|
favourite: filter.favourite || false,
|
|
137
|
-
viewUrl: filter.viewUrl
|
|
138
|
-
description: filter.description
|
|
139
|
-
jql: filter.jql
|
|
140
|
-
sharePermissions: filter.sharePermissions?.map(perm => ({
|
|
141
|
-
type: perm.type,
|
|
142
|
-
group: perm.group,
|
|
143
|
-
project: perm.project
|
|
144
|
-
})) || []
|
|
137
|
+
viewUrl: filter.viewUrl,
|
|
138
|
+
description: filter.description,
|
|
139
|
+
jql: filter.jql,
|
|
145
140
|
};
|
|
146
141
|
// Handle expansions
|
|
147
142
|
if (expansionOptions.issue_count) {
|
|
@@ -156,13 +151,13 @@ async function handleGetFilter(jiraClient, args) {
|
|
|
156
151
|
// Continue even if issue count fails
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
|
-
//
|
|
160
|
-
const
|
|
154
|
+
// Render to markdown
|
|
155
|
+
const markdown = MarkdownRenderer.renderFilter(filterData);
|
|
161
156
|
return {
|
|
162
157
|
content: [
|
|
163
158
|
{
|
|
164
159
|
type: 'text',
|
|
165
|
-
text:
|
|
160
|
+
text: markdown,
|
|
166
161
|
},
|
|
167
162
|
],
|
|
168
163
|
};
|
|
@@ -212,25 +207,22 @@ async function handleListFilters(jiraClient, args) {
|
|
|
212
207
|
}
|
|
213
208
|
}
|
|
214
209
|
}
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
};
|
|
210
|
+
// Render to markdown with pagination info
|
|
211
|
+
let markdown = MarkdownRenderer.renderFilterList(filterDataList);
|
|
212
|
+
// Add pagination guidance
|
|
213
|
+
markdown += '\n\n---\n';
|
|
214
|
+
if (startAt + maxResults < filters.length) {
|
|
215
|
+
markdown += `Showing ${startAt + 1}-${startAt + filterDataList.length} of ${filters.length}\n`;
|
|
216
|
+
markdown += `**Next page:** Use startAt=${startAt + maxResults}`;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
markdown += `Showing all ${filterDataList.length} filter${filterDataList.length !== 1 ? 's' : ''}`;
|
|
220
|
+
}
|
|
229
221
|
return {
|
|
230
222
|
content: [
|
|
231
223
|
{
|
|
232
224
|
type: 'text',
|
|
233
|
-
text:
|
|
225
|
+
text: markdown,
|
|
234
226
|
},
|
|
235
227
|
],
|
|
236
228
|
};
|
|
@@ -319,17 +311,18 @@ async function handleExecuteFilter(jiraClient, _args) {
|
|
|
319
311
|
const filterId = _args.filterId;
|
|
320
312
|
// Get issues for the filter
|
|
321
313
|
const issues = await jiraClient.getFilterIssues(filterId);
|
|
314
|
+
// Render to markdown
|
|
315
|
+
const markdown = MarkdownRenderer.renderIssueSearchResults(issues, {
|
|
316
|
+
startAt: 0,
|
|
317
|
+
maxResults: issues.length,
|
|
318
|
+
total: issues.length,
|
|
319
|
+
hasMore: false,
|
|
320
|
+
}, `filter = ${filterId}`);
|
|
322
321
|
return {
|
|
323
322
|
content: [
|
|
324
323
|
{
|
|
325
324
|
type: 'text',
|
|
326
|
-
text:
|
|
327
|
-
data: issues,
|
|
328
|
-
_metadata: {
|
|
329
|
-
filter_id: filterId,
|
|
330
|
-
issue_count: issues.length
|
|
331
|
-
}
|
|
332
|
-
}, null, 2),
|
|
325
|
+
text: markdown,
|
|
333
326
|
},
|
|
334
327
|
],
|
|
335
328
|
};
|
|
@@ -343,24 +336,24 @@ async function handleExecuteJql(jiraClient, args) {
|
|
|
343
336
|
const maxResults = args.maxResults !== undefined ? args.maxResults : 25;
|
|
344
337
|
try {
|
|
345
338
|
console.error(`Executing JQL search with args:`, JSON.stringify(args, null, 2));
|
|
346
|
-
// Parse search expansion options
|
|
347
|
-
const
|
|
339
|
+
// Parse search expansion options (not currently used but reserved for future)
|
|
340
|
+
const _searchExpansionOptions = {};
|
|
348
341
|
if (args.expand) {
|
|
349
342
|
for (const expansion of args.expand) {
|
|
350
343
|
if (['issue_details', 'transitions', 'comments_preview'].includes(expansion)) {
|
|
351
|
-
|
|
344
|
+
_searchExpansionOptions[expansion] = true;
|
|
352
345
|
}
|
|
353
346
|
}
|
|
354
347
|
}
|
|
355
348
|
// Execute the search
|
|
356
349
|
const searchResult = await jiraClient.searchIssues(args.jql, startAt, maxResults);
|
|
357
|
-
//
|
|
358
|
-
const
|
|
350
|
+
// Render directly to markdown for token efficiency
|
|
351
|
+
const markdown = MarkdownRenderer.renderIssueSearchResults(searchResult.issues, searchResult.pagination, args.jql);
|
|
359
352
|
return {
|
|
360
353
|
content: [
|
|
361
354
|
{
|
|
362
355
|
type: 'text',
|
|
363
|
-
text:
|
|
356
|
+
text: markdown,
|
|
364
357
|
},
|
|
365
358
|
],
|
|
366
359
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { MarkdownRenderer } from '../mcp/markdown-renderer.js';
|
|
3
3
|
// Helper function to normalize parameter names (support both snake_case and camelCase)
|
|
4
4
|
function normalizeArgs(args) {
|
|
5
5
|
const normalized = {};
|
|
@@ -141,13 +141,13 @@ async function handleGetIssue(jiraClient, args) {
|
|
|
141
141
|
if (expansionOptions.transitions) {
|
|
142
142
|
transitions = await jiraClient.getTransitions(args.issueKey);
|
|
143
143
|
}
|
|
144
|
-
//
|
|
145
|
-
const
|
|
144
|
+
// Render to markdown
|
|
145
|
+
const markdown = MarkdownRenderer.renderIssue(issue, transitions);
|
|
146
146
|
return {
|
|
147
147
|
content: [
|
|
148
148
|
{
|
|
149
149
|
type: 'text',
|
|
150
|
-
text:
|
|
150
|
+
text: markdown,
|
|
151
151
|
},
|
|
152
152
|
],
|
|
153
153
|
};
|
|
@@ -163,28 +163,28 @@ async function handleCreateIssue(jiraClient, args) {
|
|
|
163
163
|
labels: args.labels,
|
|
164
164
|
customFields: args.customFields
|
|
165
165
|
});
|
|
166
|
-
// Get the created issue to
|
|
166
|
+
// Get the created issue and render to markdown
|
|
167
167
|
const createdIssue = await jiraClient.getIssue(result.key, false, false);
|
|
168
|
-
const
|
|
168
|
+
const markdown = MarkdownRenderer.renderIssue(createdIssue);
|
|
169
169
|
return {
|
|
170
170
|
content: [
|
|
171
171
|
{
|
|
172
172
|
type: 'text',
|
|
173
|
-
text:
|
|
173
|
+
text: `# Issue Created\n\n${markdown}`,
|
|
174
174
|
},
|
|
175
175
|
],
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
async function handleUpdateIssue(jiraClient, args) {
|
|
179
179
|
await jiraClient.updateIssue(args.issueKey, args.summary, args.description, args.parent);
|
|
180
|
-
// Get the updated issue to
|
|
180
|
+
// Get the updated issue and render to markdown
|
|
181
181
|
const updatedIssue = await jiraClient.getIssue(args.issueKey, false, false);
|
|
182
|
-
const
|
|
182
|
+
const markdown = MarkdownRenderer.renderIssue(updatedIssue);
|
|
183
183
|
return {
|
|
184
184
|
content: [
|
|
185
185
|
{
|
|
186
186
|
type: 'text',
|
|
187
|
-
text:
|
|
187
|
+
text: `# Issue Updated\n\n${markdown}`,
|
|
188
188
|
},
|
|
189
189
|
],
|
|
190
190
|
};
|
|
@@ -212,28 +212,28 @@ async function handleDeleteIssue(_jiraClient, _args) {
|
|
|
212
212
|
}
|
|
213
213
|
async function handleTransitionIssue(jiraClient, args) {
|
|
214
214
|
await jiraClient.transitionIssue(args.issueKey, args.transitionId, args.comment);
|
|
215
|
-
// Get the updated issue to
|
|
215
|
+
// Get the updated issue and render to markdown
|
|
216
216
|
const updatedIssue = await jiraClient.getIssue(args.issueKey, false, false);
|
|
217
|
-
const
|
|
217
|
+
const markdown = MarkdownRenderer.renderIssue(updatedIssue);
|
|
218
218
|
return {
|
|
219
219
|
content: [
|
|
220
220
|
{
|
|
221
221
|
type: 'text',
|
|
222
|
-
text:
|
|
222
|
+
text: `# Issue Transitioned\n\n${markdown}`,
|
|
223
223
|
},
|
|
224
224
|
],
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
async function handleCommentIssue(jiraClient, args) {
|
|
228
228
|
await jiraClient.addComment(args.issueKey, args.comment);
|
|
229
|
-
// Get the updated issue with comments to
|
|
229
|
+
// Get the updated issue with comments and render to markdown
|
|
230
230
|
const updatedIssue = await jiraClient.getIssue(args.issueKey, true, false);
|
|
231
|
-
const
|
|
231
|
+
const markdown = MarkdownRenderer.renderIssue(updatedIssue);
|
|
232
232
|
return {
|
|
233
233
|
content: [
|
|
234
234
|
{
|
|
235
235
|
type: 'text',
|
|
236
|
-
text:
|
|
236
|
+
text: `# Comment Added\n\n${markdown}`,
|
|
237
237
|
},
|
|
238
238
|
],
|
|
239
239
|
};
|
|
@@ -242,14 +242,14 @@ async function handleLinkIssue(jiraClient, args) {
|
|
|
242
242
|
console.error(`Linking issue ${args.issueKey} to ${args.linkedIssueKey} with type ${args.linkType}`);
|
|
243
243
|
// Link the issues
|
|
244
244
|
await jiraClient.linkIssues(args.issueKey, args.linkedIssueKey, args.linkType, args.comment);
|
|
245
|
-
// Get the updated issue to
|
|
245
|
+
// Get the updated issue and render to markdown
|
|
246
246
|
const updatedIssue = await jiraClient.getIssue(args.issueKey, false, false);
|
|
247
|
-
const
|
|
247
|
+
const markdown = MarkdownRenderer.renderIssue(updatedIssue);
|
|
248
248
|
return {
|
|
249
249
|
content: [
|
|
250
250
|
{
|
|
251
251
|
type: 'text',
|
|
252
|
-
text:
|
|
252
|
+
text: `# Issue Linked\n\n${markdown}`,
|
|
253
253
|
},
|
|
254
254
|
],
|
|
255
255
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { MarkdownRenderer } from '../mcp/markdown-renderer.js';
|
|
3
3
|
// Helper function to normalize parameter names (support both snake_case and camelCase)
|
|
4
4
|
function normalizeArgs(args) {
|
|
5
5
|
const normalized = {};
|
|
@@ -122,12 +122,10 @@ async function handleGetProject(jiraClient, args) {
|
|
|
122
122
|
}
|
|
123
123
|
// Convert to ProjectData format
|
|
124
124
|
const projectData = {
|
|
125
|
-
id: project.id,
|
|
126
125
|
key: project.key,
|
|
127
126
|
name: project.name,
|
|
128
|
-
description: project.description,
|
|
129
|
-
lead: project.lead,
|
|
130
|
-
url: project.url
|
|
127
|
+
description: project.description || undefined,
|
|
128
|
+
lead: project.lead || undefined,
|
|
131
129
|
};
|
|
132
130
|
// If status counts are requested, get them
|
|
133
131
|
if (includeStatusCounts) {
|
|
@@ -140,7 +138,7 @@ async function handleGetProject(jiraClient, args) {
|
|
|
140
138
|
const status = issue.status;
|
|
141
139
|
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
142
140
|
}
|
|
143
|
-
projectData.
|
|
141
|
+
projectData.statusCounts = statusCounts;
|
|
144
142
|
}
|
|
145
143
|
catch (error) {
|
|
146
144
|
console.error(`Error getting status counts for project ${projectKey}:`, error);
|
|
@@ -167,20 +165,32 @@ async function handleGetProject(jiraClient, args) {
|
|
|
167
165
|
// Get recent issues for this project
|
|
168
166
|
const searchResult = await jiraClient.searchIssues(`project = ${projectKey} ORDER BY updated DESC`, 0, 5);
|
|
169
167
|
// Add recent issues to the response
|
|
170
|
-
projectData.
|
|
168
|
+
projectData.recentIssues = searchResult.issues.map(issue => ({
|
|
169
|
+
key: issue.key,
|
|
170
|
+
summary: issue.summary,
|
|
171
|
+
status: issue.status
|
|
172
|
+
}));
|
|
171
173
|
}
|
|
172
174
|
catch (error) {
|
|
173
175
|
console.error(`Error getting recent issues for project ${projectKey}:`, error);
|
|
174
176
|
// Continue even if recent issues fail
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
|
-
//
|
|
178
|
-
const
|
|
179
|
+
// Render to markdown
|
|
180
|
+
const markdown = MarkdownRenderer.renderProject({
|
|
181
|
+
key: projectData.key,
|
|
182
|
+
name: projectData.name,
|
|
183
|
+
description: projectData.description,
|
|
184
|
+
lead: projectData.lead,
|
|
185
|
+
statusCounts: projectData.statusCounts,
|
|
186
|
+
boards: projectData.boards,
|
|
187
|
+
recentIssues: projectData.recentIssues,
|
|
188
|
+
});
|
|
179
189
|
return {
|
|
180
190
|
content: [
|
|
181
191
|
{
|
|
182
192
|
type: 'text',
|
|
183
|
-
text:
|
|
193
|
+
text: markdown,
|
|
184
194
|
},
|
|
185
195
|
],
|
|
186
196
|
};
|
|
@@ -271,12 +281,10 @@ async function handleListProjects(jiraClient, args) {
|
|
|
271
281
|
const paginatedProjects = projects.slice(startAt, startAt + maxResults);
|
|
272
282
|
// Convert to ProjectData format
|
|
273
283
|
const projectDataList = paginatedProjects.map(project => ({
|
|
274
|
-
id: project.id,
|
|
275
284
|
key: project.key,
|
|
276
285
|
name: project.name,
|
|
277
|
-
description: project.description,
|
|
278
|
-
lead: project.lead,
|
|
279
|
-
url: project.url
|
|
286
|
+
description: project.description || undefined,
|
|
287
|
+
lead: project.lead || undefined,
|
|
280
288
|
}));
|
|
281
289
|
// If status counts are requested, get them for each project
|
|
282
290
|
if (includeStatusCounts) {
|
|
@@ -291,7 +299,7 @@ async function handleListProjects(jiraClient, args) {
|
|
|
291
299
|
const status = issue.status;
|
|
292
300
|
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
293
301
|
}
|
|
294
|
-
project.
|
|
302
|
+
project.statusCounts = statusCounts;
|
|
295
303
|
}
|
|
296
304
|
catch (error) {
|
|
297
305
|
console.error(`Error getting status counts for project ${project.key}:`, error);
|
|
@@ -299,25 +307,30 @@ async function handleListProjects(jiraClient, args) {
|
|
|
299
307
|
}
|
|
300
308
|
}
|
|
301
309
|
}
|
|
302
|
-
//
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
// Render to markdown with pagination
|
|
311
|
+
const rendererProjects = projectDataList.map(project => ({
|
|
312
|
+
key: project.key,
|
|
313
|
+
name: project.name,
|
|
314
|
+
description: project.description,
|
|
315
|
+
lead: project.lead,
|
|
316
|
+
statusCounts: project.statusCounts,
|
|
317
|
+
}));
|
|
318
|
+
// Render to markdown with pagination
|
|
319
|
+
let markdown = MarkdownRenderer.renderProjectList(rendererProjects);
|
|
320
|
+
// Add pagination guidance
|
|
321
|
+
markdown += '\n\n---\n';
|
|
322
|
+
if (startAt + maxResults < projects.length) {
|
|
323
|
+
markdown += `Showing ${startAt + 1}-${startAt + projectDataList.length} of ${projects.length}\n`;
|
|
324
|
+
markdown += `**Next page:** Use startAt=${startAt + maxResults}`;
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
markdown += `Showing all ${projectDataList.length} project${projectDataList.length !== 1 ? 's' : ''}`;
|
|
328
|
+
}
|
|
316
329
|
return {
|
|
317
330
|
content: [
|
|
318
331
|
{
|
|
319
332
|
type: 'text',
|
|
320
|
-
text:
|
|
333
|
+
text: markdown,
|
|
321
334
|
},
|
|
322
335
|
],
|
|
323
336
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { MarkdownRenderer } from '../mcp/markdown-renderer.js';
|
|
3
3
|
// Helper function to normalize parameter names (support both snake_case and camelCase)
|
|
4
4
|
function normalizeArgs(args) {
|
|
5
5
|
const normalized = {};
|
|
@@ -67,22 +67,22 @@ export async function setupSearchHandlers(server, jiraClient, request) {
|
|
|
67
67
|
}
|
|
68
68
|
try {
|
|
69
69
|
console.error(`Executing search with args:`, JSON.stringify(normalizedArgs, null, 2));
|
|
70
|
-
// Parse expansion options
|
|
71
|
-
const
|
|
70
|
+
// Parse expansion options (reserved for future use)
|
|
71
|
+
const _expansionOptions = {};
|
|
72
72
|
if (normalizedArgs.expand) {
|
|
73
73
|
for (const expansion of normalizedArgs.expand) {
|
|
74
|
-
|
|
74
|
+
_expansionOptions[expansion] = true;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
// Execute the search
|
|
78
78
|
const searchResult = await jiraClient.searchIssues(normalizedArgs.jql, normalizedArgs.startAt, normalizedArgs.maxResults);
|
|
79
|
-
//
|
|
80
|
-
const
|
|
79
|
+
// Render to markdown for token efficiency
|
|
80
|
+
const markdown = MarkdownRenderer.renderIssueSearchResults(searchResult.issues, searchResult.pagination, normalizedArgs.jql);
|
|
81
81
|
return {
|
|
82
82
|
content: [
|
|
83
83
|
{
|
|
84
84
|
type: 'text',
|
|
85
|
-
text:
|
|
85
|
+
text: markdown,
|
|
86
86
|
},
|
|
87
87
|
],
|
|
88
88
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { MarkdownRenderer } from '../mcp/markdown-renderer.js';
|
|
3
3
|
// Helper function to normalize parameter names (support both snake_case and camelCase)
|
|
4
4
|
function normalizeArgs(args) {
|
|
5
5
|
const normalized = {};
|
|
@@ -164,24 +164,24 @@ async function handleGetSprint(jiraClient, args) {
|
|
|
164
164
|
if (expansionOptions.issues) {
|
|
165
165
|
issues = await jiraClient.getSprintIssues(args.sprintId);
|
|
166
166
|
}
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
167
|
+
// Note: Sprint report expansion not yet supported in markdown renderer
|
|
168
|
+
// Render to markdown
|
|
169
|
+
const markdown = MarkdownRenderer.renderSprint({
|
|
170
|
+
id: sprint.id,
|
|
171
|
+
name: sprint.name,
|
|
172
|
+
state: sprint.state,
|
|
173
|
+
boardId: sprint.boardId,
|
|
174
|
+
goal: sprint.goal,
|
|
175
|
+
startDate: sprint.startDate,
|
|
176
|
+
endDate: sprint.endDate,
|
|
177
|
+
completeDate: sprint.completeDate,
|
|
178
|
+
issues: issues,
|
|
179
|
+
});
|
|
180
180
|
return {
|
|
181
181
|
content: [
|
|
182
182
|
{
|
|
183
183
|
type: 'text',
|
|
184
|
-
text:
|
|
184
|
+
text: markdown,
|
|
185
185
|
},
|
|
186
186
|
],
|
|
187
187
|
};
|
|
@@ -189,13 +189,21 @@ async function handleGetSprint(jiraClient, args) {
|
|
|
189
189
|
async function handleCreateSprint(jiraClient, args) {
|
|
190
190
|
// Create the sprint
|
|
191
191
|
const response = await jiraClient.createSprint(args.boardId, args.name, args.startDate, args.endDate, args.goal);
|
|
192
|
-
//
|
|
193
|
-
const
|
|
192
|
+
// Render to markdown
|
|
193
|
+
const markdown = MarkdownRenderer.renderSprint({
|
|
194
|
+
id: response.id,
|
|
195
|
+
name: response.name,
|
|
196
|
+
state: response.state,
|
|
197
|
+
boardId: response.boardId,
|
|
198
|
+
goal: response.goal,
|
|
199
|
+
startDate: response.startDate,
|
|
200
|
+
endDate: response.endDate,
|
|
201
|
+
});
|
|
194
202
|
return {
|
|
195
203
|
content: [
|
|
196
204
|
{
|
|
197
205
|
type: 'text',
|
|
198
|
-
text:
|
|
206
|
+
text: `# Sprint Created\n\n${markdown}`,
|
|
199
207
|
},
|
|
200
208
|
],
|
|
201
209
|
};
|
|
@@ -212,13 +220,21 @@ async function handleUpdateSprint(jiraClient, args) {
|
|
|
212
220
|
await jiraClient.updateSprint(args.sprintId, args.name, args.goal, args.startDate, args.endDate, args.state);
|
|
213
221
|
// Get the updated sprint
|
|
214
222
|
const updatedSprint = await jiraClient.getSprint(args.sprintId);
|
|
215
|
-
//
|
|
216
|
-
const
|
|
223
|
+
// Render to markdown
|
|
224
|
+
const markdown = MarkdownRenderer.renderSprint({
|
|
225
|
+
id: updatedSprint.id,
|
|
226
|
+
name: updatedSprint.name,
|
|
227
|
+
state: updatedSprint.state,
|
|
228
|
+
boardId: updatedSprint.boardId,
|
|
229
|
+
goal: updatedSprint.goal,
|
|
230
|
+
startDate: updatedSprint.startDate,
|
|
231
|
+
endDate: updatedSprint.endDate,
|
|
232
|
+
});
|
|
217
233
|
return {
|
|
218
234
|
content: [
|
|
219
235
|
{
|
|
220
236
|
type: 'text',
|
|
221
|
-
text:
|
|
237
|
+
text: `# Sprint Updated\n\n${markdown}`,
|
|
222
238
|
},
|
|
223
239
|
],
|
|
224
240
|
};
|
|
@@ -244,10 +260,7 @@ async function handleDeleteSprint(jiraClient, args) {
|
|
|
244
260
|
content: [
|
|
245
261
|
{
|
|
246
262
|
type: 'text',
|
|
247
|
-
text:
|
|
248
|
-
success: true,
|
|
249
|
-
message: `Sprint ${args.sprintId} has been deleted successfully.`,
|
|
250
|
-
}, null, 2),
|
|
263
|
+
text: `# Sprint Deleted\n\nSprint ${args.sprintId} has been deleted successfully.`,
|
|
251
264
|
},
|
|
252
265
|
],
|
|
253
266
|
};
|
|
@@ -258,17 +271,46 @@ async function handleListSprints(jiraClient, args) {
|
|
|
258
271
|
const maxResults = args.maxResults !== undefined ? args.maxResults : 50;
|
|
259
272
|
// Get sprints
|
|
260
273
|
const response = await jiraClient.listSprints(args.boardId, args.state, startAt, maxResults);
|
|
261
|
-
//
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
274
|
+
// Render sprints to markdown
|
|
275
|
+
const lines = [];
|
|
276
|
+
lines.push(`# Sprints (${response.total})`);
|
|
277
|
+
if (args.state) {
|
|
278
|
+
lines.push(`**Filter:** ${args.state}`);
|
|
279
|
+
}
|
|
280
|
+
lines.push('');
|
|
281
|
+
// Group by state
|
|
282
|
+
const byState = {};
|
|
283
|
+
for (const sprint of response.sprints) {
|
|
284
|
+
const state = sprint.state || 'unknown';
|
|
285
|
+
if (!byState[state])
|
|
286
|
+
byState[state] = [];
|
|
287
|
+
byState[state].push(sprint);
|
|
288
|
+
}
|
|
289
|
+
for (const [state, sprints] of Object.entries(byState)) {
|
|
290
|
+
const stateIcon = state === 'active' ? '[>]' : state === 'closed' ? '[x]' : '[ ]';
|
|
291
|
+
lines.push(`## ${state.charAt(0).toUpperCase() + state.slice(1)} ${stateIcon}`);
|
|
292
|
+
for (const sprint of sprints) {
|
|
293
|
+
lines.push(`- **${sprint.name}** (id: ${sprint.id})`);
|
|
294
|
+
if (sprint.goal) {
|
|
295
|
+
lines.push(` Goal: ${sprint.goal.substring(0, 80)}${sprint.goal.length > 80 ? '...' : ''}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
lines.push('');
|
|
299
|
+
}
|
|
300
|
+
// Pagination
|
|
301
|
+
lines.push('---');
|
|
302
|
+
if (startAt + maxResults < response.total) {
|
|
303
|
+
lines.push(`Showing ${startAt + 1}-${startAt + response.sprints.length} of ${response.total}`);
|
|
304
|
+
lines.push(`**Next page:** Use startAt=${startAt + maxResults}`);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
lines.push(`Showing all ${response.sprints.length} sprint${response.sprints.length !== 1 ? 's' : ''}`);
|
|
308
|
+
}
|
|
267
309
|
return {
|
|
268
310
|
content: [
|
|
269
311
|
{
|
|
270
312
|
type: 'text',
|
|
271
|
-
text:
|
|
313
|
+
text: lines.join('\n'),
|
|
272
314
|
},
|
|
273
315
|
],
|
|
274
316
|
};
|
|
@@ -291,18 +333,22 @@ async function handleManageIssues(jiraClient, args) {
|
|
|
291
333
|
// Get the updated sprint with issues
|
|
292
334
|
const sprint = await jiraClient.getSprint(args.sprintId);
|
|
293
335
|
const issues = await jiraClient.getSprintIssues(args.sprintId);
|
|
294
|
-
//
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
336
|
+
// Render to markdown
|
|
337
|
+
const markdown = MarkdownRenderer.renderSprint({
|
|
338
|
+
id: sprint.id,
|
|
339
|
+
name: sprint.name,
|
|
340
|
+
state: sprint.state,
|
|
341
|
+
boardId: sprint.boardId,
|
|
342
|
+
goal: sprint.goal,
|
|
343
|
+
startDate: sprint.startDate,
|
|
344
|
+
endDate: sprint.endDate,
|
|
345
|
+
issues: issues,
|
|
346
|
+
});
|
|
301
347
|
return {
|
|
302
348
|
content: [
|
|
303
349
|
{
|
|
304
350
|
type: 'text',
|
|
305
|
-
text:
|
|
351
|
+
text: `# Sprint Issues Updated\n\n${markdown}`,
|
|
306
352
|
},
|
|
307
353
|
],
|
|
308
354
|
};
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown renderer for MCP tool responses
|
|
3
|
+
*
|
|
4
|
+
* Converts structured JSON responses to token-efficient markdown
|
|
5
|
+
* that's optimized for AI assistant consumption.
|
|
6
|
+
*
|
|
7
|
+
* Design principles:
|
|
8
|
+
* - Minimal tokens, maximum clarity
|
|
9
|
+
* - Embedded navigation hints (suggested next actions)
|
|
10
|
+
* - Human-readable pagination guidance
|
|
11
|
+
* - Plain text structure over decorative formatting
|
|
12
|
+
*/
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Helper Functions
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Format a date string to a more readable format
|
|
18
|
+
*/
|
|
19
|
+
function formatDate(dateStr) {
|
|
20
|
+
if (!dateStr)
|
|
21
|
+
return 'Not set';
|
|
22
|
+
const date = new Date(dateStr);
|
|
23
|
+
return date.toLocaleDateString('en-US', {
|
|
24
|
+
year: 'numeric',
|
|
25
|
+
month: 'short',
|
|
26
|
+
day: 'numeric'
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Format status with visual indicator
|
|
31
|
+
*/
|
|
32
|
+
function formatStatus(status) {
|
|
33
|
+
const statusIcons = {
|
|
34
|
+
'Done': '[x]',
|
|
35
|
+
'Closed': '[x]',
|
|
36
|
+
'Resolved': '[x]',
|
|
37
|
+
'In Progress': '[>]',
|
|
38
|
+
'In Review': '[>]',
|
|
39
|
+
'To Do': '[ ]',
|
|
40
|
+
'Open': '[ ]',
|
|
41
|
+
'Backlog': '[-]',
|
|
42
|
+
};
|
|
43
|
+
const icon = statusIcons[status] || '[?]';
|
|
44
|
+
return `${icon} ${status}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Truncate text to a maximum length with ellipsis
|
|
48
|
+
*/
|
|
49
|
+
function truncate(text, maxLength = 150) {
|
|
50
|
+
if (!text)
|
|
51
|
+
return '';
|
|
52
|
+
const cleaned = text.replace(/\n+/g, ' ').trim();
|
|
53
|
+
if (cleaned.length <= maxLength)
|
|
54
|
+
return cleaned;
|
|
55
|
+
return cleaned.substring(0, maxLength).trim() + '...';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Strip HTML tags for plain text display
|
|
59
|
+
*/
|
|
60
|
+
function stripHtml(html) {
|
|
61
|
+
return html
|
|
62
|
+
.replace(/<[^>]*>/g, '')
|
|
63
|
+
.replace(/ /g, ' ')
|
|
64
|
+
.replace(/&/g, '&')
|
|
65
|
+
.replace(/</g, '<')
|
|
66
|
+
.replace(/>/g, '>')
|
|
67
|
+
.replace(/\s+/g, ' ')
|
|
68
|
+
.trim();
|
|
69
|
+
}
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Issue Rendering
|
|
72
|
+
// ============================================================================
|
|
73
|
+
/**
|
|
74
|
+
* Render a single issue as markdown
|
|
75
|
+
*/
|
|
76
|
+
export function renderIssue(issue, transitions) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
lines.push(`# ${issue.key}: ${issue.summary}`);
|
|
79
|
+
lines.push('');
|
|
80
|
+
// Core fields
|
|
81
|
+
lines.push(`**Status:** ${formatStatus(issue.status)}`);
|
|
82
|
+
if (issue.assignee) {
|
|
83
|
+
lines.push(`**Assignee:** ${issue.assignee}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
lines.push(`**Assignee:** Unassigned`);
|
|
87
|
+
}
|
|
88
|
+
lines.push(`**Reporter:** ${issue.reporter}`);
|
|
89
|
+
if (issue.parent) {
|
|
90
|
+
lines.push(`**Parent:** ${issue.parent}`);
|
|
91
|
+
}
|
|
92
|
+
if (issue.dueDate) {
|
|
93
|
+
lines.push(`**Due:** ${formatDate(issue.dueDate)}`);
|
|
94
|
+
}
|
|
95
|
+
if (issue.storyPoints) {
|
|
96
|
+
lines.push(`**Points:** ${issue.storyPoints}`);
|
|
97
|
+
}
|
|
98
|
+
if (issue.resolution) {
|
|
99
|
+
lines.push(`**Resolution:** ${issue.resolution}`);
|
|
100
|
+
}
|
|
101
|
+
// Description (truncated for token efficiency)
|
|
102
|
+
if (issue.description) {
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push('## Description');
|
|
105
|
+
const desc = stripHtml(issue.description);
|
|
106
|
+
lines.push(truncate(desc, 300));
|
|
107
|
+
}
|
|
108
|
+
// Issue links
|
|
109
|
+
if (issue.issueLinks && issue.issueLinks.length > 0) {
|
|
110
|
+
lines.push('');
|
|
111
|
+
lines.push('## Links');
|
|
112
|
+
for (const link of issue.issueLinks) {
|
|
113
|
+
if (link.outward) {
|
|
114
|
+
lines.push(`- ${link.type} -> ${link.outward}`);
|
|
115
|
+
}
|
|
116
|
+
if (link.inward) {
|
|
117
|
+
lines.push(`- ${link.type} <- ${link.inward}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Comments (if present)
|
|
122
|
+
if (issue.comments && issue.comments.length > 0) {
|
|
123
|
+
lines.push('');
|
|
124
|
+
lines.push(`## Comments (${issue.comments.length})`);
|
|
125
|
+
// Show last 3 comments
|
|
126
|
+
const recentComments = issue.comments.slice(-3);
|
|
127
|
+
for (const comment of recentComments) {
|
|
128
|
+
lines.push(`- **${comment.author}** (${formatDate(comment.created)}): ${truncate(stripHtml(comment.body), 100)}`);
|
|
129
|
+
}
|
|
130
|
+
if (issue.comments.length > 3) {
|
|
131
|
+
lines.push(` ... and ${issue.comments.length - 3} more comments`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Available transitions
|
|
135
|
+
if (transitions && transitions.length > 0) {
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push('## Available Actions');
|
|
138
|
+
for (const t of transitions) {
|
|
139
|
+
lines.push(`- **${t.name}** -> ${t.to.name} (id: ${t.id})`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Render issue search results as markdown
|
|
146
|
+
*/
|
|
147
|
+
export function renderIssueSearchResults(issues, pagination, jql) {
|
|
148
|
+
const lines = [];
|
|
149
|
+
// Header
|
|
150
|
+
if (jql) {
|
|
151
|
+
lines.push(`# Search Results`);
|
|
152
|
+
lines.push(`**JQL:** \`${jql}\``);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
lines.push('# Issues');
|
|
156
|
+
}
|
|
157
|
+
lines.push(`Found ${pagination.total} issue${pagination.total !== 1 ? 's' : ''}`);
|
|
158
|
+
lines.push('');
|
|
159
|
+
// Status summary
|
|
160
|
+
const statusCounts = {};
|
|
161
|
+
for (const issue of issues) {
|
|
162
|
+
statusCounts[issue.status] = (statusCounts[issue.status] || 0) + 1;
|
|
163
|
+
}
|
|
164
|
+
if (Object.keys(statusCounts).length > 1) {
|
|
165
|
+
lines.push('**By Status:** ' + Object.entries(statusCounts)
|
|
166
|
+
.map(([status, count]) => `${status}: ${count}`)
|
|
167
|
+
.join(', '));
|
|
168
|
+
lines.push('');
|
|
169
|
+
}
|
|
170
|
+
// Issues list
|
|
171
|
+
for (let i = 0; i < issues.length; i++) {
|
|
172
|
+
const issue = issues[i];
|
|
173
|
+
const num = pagination.startAt + i + 1;
|
|
174
|
+
lines.push(`## ${num}. ${issue.key}: ${issue.summary}`);
|
|
175
|
+
lines.push(`${formatStatus(issue.status)} | ${issue.assignee || 'Unassigned'}`);
|
|
176
|
+
if (issue.dueDate) {
|
|
177
|
+
lines.push(`Due: ${formatDate(issue.dueDate)}`);
|
|
178
|
+
}
|
|
179
|
+
if (issue.description) {
|
|
180
|
+
const desc = stripHtml(issue.description);
|
|
181
|
+
if (desc.length > 0) {
|
|
182
|
+
lines.push(`> ${truncate(desc, 120)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
// Pagination guidance
|
|
188
|
+
lines.push('---');
|
|
189
|
+
if (pagination.hasMore) {
|
|
190
|
+
const nextOffset = pagination.startAt + pagination.maxResults;
|
|
191
|
+
lines.push(`Showing ${pagination.startAt + 1}-${pagination.startAt + issues.length} of ${pagination.total}`);
|
|
192
|
+
lines.push(`**Next page:** Use startAt=${nextOffset}`);
|
|
193
|
+
}
|
|
194
|
+
else if (pagination.startAt > 0) {
|
|
195
|
+
lines.push(`Showing ${pagination.startAt + 1}-${pagination.startAt + issues.length} of ${pagination.total} (last page)`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
lines.push(`Showing all ${issues.length} result${issues.length !== 1 ? 's' : ''}`);
|
|
199
|
+
}
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
export function renderProject(project) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
lines.push(`# ${project.key}: ${project.name}`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
if (project.lead) {
|
|
207
|
+
lines.push(`**Lead:** ${project.lead}`);
|
|
208
|
+
}
|
|
209
|
+
if (project.projectTypeKey) {
|
|
210
|
+
lines.push(`**Type:** ${project.projectTypeKey}`);
|
|
211
|
+
}
|
|
212
|
+
if (project.description) {
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push(truncate(stripHtml(project.description), 200));
|
|
215
|
+
}
|
|
216
|
+
// Status counts
|
|
217
|
+
if (project.statusCounts && Object.keys(project.statusCounts).length > 0) {
|
|
218
|
+
lines.push('');
|
|
219
|
+
lines.push('## Issue Summary');
|
|
220
|
+
const total = Object.values(project.statusCounts).reduce((a, b) => a + b, 0);
|
|
221
|
+
lines.push(`Total: ${total} issues`);
|
|
222
|
+
for (const [status, count] of Object.entries(project.statusCounts)) {
|
|
223
|
+
lines.push(`- ${status}: ${count}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Boards
|
|
227
|
+
if (project.boards && project.boards.length > 0) {
|
|
228
|
+
lines.push('');
|
|
229
|
+
lines.push('## Boards');
|
|
230
|
+
for (const board of project.boards) {
|
|
231
|
+
lines.push(`- ${board.name} (${board.type}, id: ${board.id})`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Recent issues
|
|
235
|
+
if (project.recentIssues && project.recentIssues.length > 0) {
|
|
236
|
+
lines.push('');
|
|
237
|
+
lines.push('## Recent Issues');
|
|
238
|
+
for (const issue of project.recentIssues) {
|
|
239
|
+
lines.push(`- ${issue.key}: ${issue.summary} [${issue.status}]`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return lines.join('\n');
|
|
243
|
+
}
|
|
244
|
+
export function renderProjectList(projects) {
|
|
245
|
+
const lines = [];
|
|
246
|
+
lines.push(`# Projects (${projects.length})`);
|
|
247
|
+
lines.push('');
|
|
248
|
+
for (const project of projects) {
|
|
249
|
+
lines.push(`## ${project.key}: ${project.name}`);
|
|
250
|
+
if (project.lead) {
|
|
251
|
+
lines.push(`Lead: ${project.lead}`);
|
|
252
|
+
}
|
|
253
|
+
if (project.description) {
|
|
254
|
+
lines.push(`> ${truncate(stripHtml(project.description), 100)}`);
|
|
255
|
+
}
|
|
256
|
+
if (project.statusCounts) {
|
|
257
|
+
const total = Object.values(project.statusCounts).reduce((a, b) => a + b, 0);
|
|
258
|
+
lines.push(`Issues: ${total}`);
|
|
259
|
+
}
|
|
260
|
+
lines.push('');
|
|
261
|
+
}
|
|
262
|
+
lines.push('---');
|
|
263
|
+
lines.push(`Tip: Use manage_jira_project with operation="get" and projectKey="KEY" for details`);
|
|
264
|
+
return lines.join('\n');
|
|
265
|
+
}
|
|
266
|
+
export function renderBoard(board) {
|
|
267
|
+
const lines = [];
|
|
268
|
+
lines.push(`# Board: ${board.name}`);
|
|
269
|
+
lines.push(`**ID:** ${board.id}`);
|
|
270
|
+
lines.push(`**Type:** ${board.type}`);
|
|
271
|
+
if (board.projectKey) {
|
|
272
|
+
lines.push(`**Project:** ${board.projectKey} (${board.projectName || ''})`);
|
|
273
|
+
}
|
|
274
|
+
if (board.sprints && board.sprints.length > 0) {
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push('## Sprints');
|
|
277
|
+
for (const sprint of board.sprints) {
|
|
278
|
+
const stateIcon = sprint.state === 'active' ? '[>]' : sprint.state === 'closed' ? '[x]' : '[ ]';
|
|
279
|
+
lines.push(`- ${stateIcon} ${sprint.name} (id: ${sprint.id})`);
|
|
280
|
+
if (sprint.goal) {
|
|
281
|
+
lines.push(` Goal: ${truncate(sprint.goal, 80)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return lines.join('\n');
|
|
286
|
+
}
|
|
287
|
+
export function renderBoardList(boards) {
|
|
288
|
+
const lines = [];
|
|
289
|
+
lines.push(`# Boards (${boards.length})`);
|
|
290
|
+
lines.push('');
|
|
291
|
+
// Group by type
|
|
292
|
+
const byType = {};
|
|
293
|
+
for (const board of boards) {
|
|
294
|
+
const type = board.type || 'other';
|
|
295
|
+
if (!byType[type])
|
|
296
|
+
byType[type] = [];
|
|
297
|
+
byType[type].push(board);
|
|
298
|
+
}
|
|
299
|
+
for (const [type, typeBoards] of Object.entries(byType)) {
|
|
300
|
+
lines.push(`## ${type.charAt(0).toUpperCase() + type.slice(1)} Boards`);
|
|
301
|
+
for (const board of typeBoards) {
|
|
302
|
+
lines.push(`- **${board.name}** (id: ${board.id})${board.projectKey ? ` - ${board.projectKey}` : ''}`);
|
|
303
|
+
}
|
|
304
|
+
lines.push('');
|
|
305
|
+
}
|
|
306
|
+
return lines.join('\n');
|
|
307
|
+
}
|
|
308
|
+
export function renderSprint(sprint) {
|
|
309
|
+
const lines = [];
|
|
310
|
+
const stateIcon = sprint.state === 'active' ? '[ACTIVE]' : sprint.state === 'closed' ? '[CLOSED]' : '[FUTURE]';
|
|
311
|
+
lines.push(`# Sprint: ${sprint.name} ${stateIcon}`);
|
|
312
|
+
lines.push(`**ID:** ${sprint.id}`);
|
|
313
|
+
lines.push(`**Board:** ${sprint.boardId}`);
|
|
314
|
+
if (sprint.startDate) {
|
|
315
|
+
lines.push(`**Started:** ${formatDate(sprint.startDate)}`);
|
|
316
|
+
}
|
|
317
|
+
if (sprint.endDate) {
|
|
318
|
+
lines.push(`**Ends:** ${formatDate(sprint.endDate)}`);
|
|
319
|
+
}
|
|
320
|
+
if (sprint.completeDate) {
|
|
321
|
+
lines.push(`**Completed:** ${formatDate(sprint.completeDate)}`);
|
|
322
|
+
}
|
|
323
|
+
if (sprint.goal) {
|
|
324
|
+
lines.push('');
|
|
325
|
+
lines.push('## Goal');
|
|
326
|
+
lines.push(sprint.goal);
|
|
327
|
+
}
|
|
328
|
+
if (sprint.issues && sprint.issues.length > 0) {
|
|
329
|
+
lines.push('');
|
|
330
|
+
lines.push(`## Issues (${sprint.issues.length})`);
|
|
331
|
+
// Group by status
|
|
332
|
+
const byStatus = {};
|
|
333
|
+
for (const issue of sprint.issues) {
|
|
334
|
+
const status = issue.status || 'Unknown';
|
|
335
|
+
if (!byStatus[status])
|
|
336
|
+
byStatus[status] = [];
|
|
337
|
+
byStatus[status].push(issue);
|
|
338
|
+
}
|
|
339
|
+
for (const [status, statusIssues] of Object.entries(byStatus)) {
|
|
340
|
+
lines.push(`### ${status} (${statusIssues.length})`);
|
|
341
|
+
for (const issue of statusIssues) {
|
|
342
|
+
lines.push(`- ${issue.key}: ${issue.summary}${issue.assignee ? ` [${issue.assignee}]` : ''}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return lines.join('\n');
|
|
347
|
+
}
|
|
348
|
+
export function renderFilter(filter) {
|
|
349
|
+
const lines = [];
|
|
350
|
+
lines.push(`# Filter: ${filter.name}`);
|
|
351
|
+
lines.push(`**ID:** ${filter.id}`);
|
|
352
|
+
lines.push(`**Owner:** ${filter.owner}`);
|
|
353
|
+
lines.push(`**Favorite:** ${filter.favourite ? 'Yes' : 'No'}`);
|
|
354
|
+
if (filter.jql) {
|
|
355
|
+
lines.push('');
|
|
356
|
+
lines.push('## JQL');
|
|
357
|
+
lines.push('```');
|
|
358
|
+
lines.push(filter.jql);
|
|
359
|
+
lines.push('```');
|
|
360
|
+
}
|
|
361
|
+
if (filter.description) {
|
|
362
|
+
lines.push('');
|
|
363
|
+
lines.push('## Description');
|
|
364
|
+
lines.push(filter.description);
|
|
365
|
+
}
|
|
366
|
+
if (filter.issueCount !== undefined) {
|
|
367
|
+
lines.push('');
|
|
368
|
+
lines.push(`**Matches:** ${filter.issueCount} issues`);
|
|
369
|
+
}
|
|
370
|
+
lines.push('');
|
|
371
|
+
lines.push('---');
|
|
372
|
+
lines.push(`Tip: Use manage_jira_filter with operation="execute_filter" and filterId="${filter.id}" to run this filter`);
|
|
373
|
+
return lines.join('\n');
|
|
374
|
+
}
|
|
375
|
+
export function renderFilterList(filters) {
|
|
376
|
+
const lines = [];
|
|
377
|
+
lines.push(`# Filters (${filters.length})`);
|
|
378
|
+
lines.push('');
|
|
379
|
+
// Separate favorites
|
|
380
|
+
const favorites = filters.filter(f => f.favourite);
|
|
381
|
+
const others = filters.filter(f => !f.favourite);
|
|
382
|
+
if (favorites.length > 0) {
|
|
383
|
+
lines.push('## Favorites');
|
|
384
|
+
for (const filter of favorites) {
|
|
385
|
+
lines.push(`- **${filter.name}** (id: ${filter.id}) - ${filter.owner}`);
|
|
386
|
+
}
|
|
387
|
+
lines.push('');
|
|
388
|
+
}
|
|
389
|
+
if (others.length > 0) {
|
|
390
|
+
lines.push('## Other Filters');
|
|
391
|
+
for (const filter of others) {
|
|
392
|
+
lines.push(`- ${filter.name} (id: ${filter.id}) - ${filter.owner}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return lines.join('\n');
|
|
396
|
+
}
|
|
397
|
+
// ============================================================================
|
|
398
|
+
// Export convenience object
|
|
399
|
+
// ============================================================================
|
|
400
|
+
export const MarkdownRenderer = {
|
|
401
|
+
// Issues
|
|
402
|
+
renderIssue,
|
|
403
|
+
renderIssueSearchResults,
|
|
404
|
+
// Projects
|
|
405
|
+
renderProject,
|
|
406
|
+
renderProjectList,
|
|
407
|
+
// Boards
|
|
408
|
+
renderBoard,
|
|
409
|
+
renderBoardList,
|
|
410
|
+
// Sprints
|
|
411
|
+
renderSprint,
|
|
412
|
+
// Filters
|
|
413
|
+
renderFilter,
|
|
414
|
+
renderFilterList,
|
|
415
|
+
// Helpers (exposed for custom use)
|
|
416
|
+
helpers: {
|
|
417
|
+
formatDate,
|
|
418
|
+
formatStatus,
|
|
419
|
+
truncate,
|
|
420
|
+
stripHtml,
|
|
421
|
+
}
|
|
422
|
+
};
|