@aaronsb/jira-cloud-mcp 0.7.1 → 0.7.6

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.
@@ -81,22 +81,18 @@ async function handleGetProject(jiraClient, args) {
81
81
  catch {
82
82
  // Non-fatal — continue without issue types
83
83
  }
84
- // If status counts are requested, get them
84
+ // If status counts are requested, use the count API for exact totals
85
85
  if (includeStatusCounts) {
86
86
  try {
87
- // Get issue counts by status for this project
88
- const searchResult = await jiraClient.searchIssues(`project = ${projectKey}`, 0, 100);
89
- // Count issues by status
90
- const statusCounts = {};
91
- for (const issue of searchResult.issues) {
92
- const status = issue.status;
93
- statusCounts[status] = (statusCounts[status] || 0) + 1;
94
- }
95
- projectData.statusCounts = statusCounts;
87
+ const [total, open, done] = await Promise.all([
88
+ jiraClient.countIssues(`project = ${projectKey}`),
89
+ jiraClient.countIssues(`project = ${projectKey} AND resolution = Unresolved`),
90
+ jiraClient.countIssues(`project = ${projectKey} AND resolution != Unresolved`),
91
+ ]);
92
+ projectData.statusCounts = { Total: total, Open: open, Done: done };
96
93
  }
97
94
  catch (error) {
98
95
  console.error(`Error getting status counts for project ${projectKey}:`, error);
99
- // Continue even if status counts fail
100
96
  }
101
97
  }
102
98
  // Handle expansions
@@ -166,26 +162,17 @@ async function handleListProjects(jiraClient, args) {
166
162
  description: project.description || undefined,
167
163
  lead: project.lead || undefined,
168
164
  }));
169
- // If status counts are requested, get them for each project
165
+ // If status counts are requested, use count API (fast, exact, parallel)
170
166
  if (includeStatusCounts) {
171
- // This would be more efficient with a batch API call, but for now we'll do it sequentially
172
- for (const project of projectDataList) {
167
+ await Promise.all(projectDataList.map(async (project) => {
173
168
  try {
174
- // Get issue counts by status for this project
175
- const searchResult = await jiraClient.searchIssues(`project = ${project.key}`, 0, 100);
176
- // Count issues by status
177
- const statusCounts = {};
178
- for (const issue of searchResult.issues) {
179
- const status = issue.status;
180
- statusCounts[status] = (statusCounts[status] || 0) + 1;
181
- }
182
- project.statusCounts = statusCounts;
169
+ const total = await jiraClient.countIssues(`project = ${project.key}`);
170
+ project.statusCounts = { Total: total };
183
171
  }
184
172
  catch (error) {
185
- console.error(`Error getting status counts for project ${project.key}:`, error);
186
- // Continue with other projects even if one fails
173
+ console.error(`Error getting count for project ${project.key}:`, error);
187
174
  }
188
- }
175
+ }));
189
176
  }
190
177
  // Render to markdown with pagination
191
178
  const rendererProjects = projectDataList.map(project => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronsb/jira-cloud-mcp",
3
- "version": "0.7.1",
3
+ "version": "0.7.6",
4
4
  "mcpName": "io.github.aaronsb/jira-cloud",
5
5
  "description": "Model Context Protocol (MCP) server for Jira Cloud - enables AI assistants to interact with Jira",
6
6
  "type": "module",
@@ -1,103 +0,0 @@
1
- import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
- import { MarkdownRenderer } from '../mcp/markdown-renderer.js';
3
- // Helper function to normalize parameter names (support both snake_case and camelCase)
4
- function normalizeArgs(args) {
5
- const normalized = {};
6
- for (const [key, value] of Object.entries(args)) {
7
- // Convert snake_case to camelCase
8
- if (key === 'start_at') {
9
- normalized['startAt'] = value;
10
- }
11
- else if (key === 'max_results') {
12
- normalized['maxResults'] = value;
13
- }
14
- else if (key === 'filter_id') {
15
- normalized['filterId'] = value;
16
- }
17
- else {
18
- normalized[key] = value;
19
- }
20
- }
21
- return normalized;
22
- }
23
- function isSearchIssuesArgs(args) {
24
- if (typeof args !== 'object' || args === null) {
25
- throw new McpError(ErrorCode.InvalidParams, `Invalid search_jira_issues arguments: Expected an object with a jql parameter. Example: { "jql": "project = PROJ" }`);
26
- }
27
- const typedArgs = args;
28
- if (typeof typedArgs.jql !== 'string') {
29
- throw new McpError(ErrorCode.InvalidParams, `Missing or invalid jql parameter. Please provide a valid JQL query string. Example: { "jql": "project = PROJ" }`);
30
- }
31
- // Validate startAt if present
32
- if (typedArgs.startAt !== undefined && typeof typedArgs.startAt !== 'number') {
33
- throw new McpError(ErrorCode.InvalidParams, 'Invalid startAt parameter. Expected a number.');
34
- }
35
- // Validate maxResults if present
36
- if (typedArgs.maxResults !== undefined && typeof typedArgs.maxResults !== 'number') {
37
- throw new McpError(ErrorCode.InvalidParams, 'Invalid maxResults parameter. Expected a number.');
38
- }
39
- // Validate expand parameter if present
40
- if (typedArgs.expand !== undefined) {
41
- if (!Array.isArray(typedArgs.expand)) {
42
- throw new McpError(ErrorCode.InvalidParams, 'Invalid expand parameter. Expected an array of strings.');
43
- }
44
- const validExpansions = ['issue_details', 'transitions', 'comments_preview'];
45
- for (const expansion of typedArgs.expand) {
46
- if (typeof expansion !== 'string' || !validExpansions.includes(expansion)) {
47
- throw new McpError(ErrorCode.InvalidParams, `Invalid expansion: ${expansion}. Valid expansions are: ${validExpansions.join(', ')}`);
48
- }
49
- }
50
- }
51
- return true;
52
- }
53
- export async function setupSearchHandlers(server, jiraClient, request) {
54
- console.error('Handling search request...');
55
- const { name } = request.params;
56
- const args = request.params.arguments;
57
- if (!args) {
58
- throw new McpError(ErrorCode.InvalidParams, 'Missing arguments');
59
- }
60
- // Normalize arguments to support both snake_case and camelCase
61
- const normalizedArgs = normalizeArgs(args);
62
- switch (name) {
63
- case 'search_jira_issues': {
64
- console.error('Processing search_jira_issues request');
65
- if (!isSearchIssuesArgs(normalizedArgs)) {
66
- throw new McpError(ErrorCode.InvalidParams, 'Invalid search_jira_issues arguments');
67
- }
68
- try {
69
- console.error(`Executing search with args:`, JSON.stringify(normalizedArgs, null, 2));
70
- // Parse expansion options (reserved for future use)
71
- const _expansionOptions = {};
72
- if (normalizedArgs.expand) {
73
- for (const expansion of normalizedArgs.expand) {
74
- _expansionOptions[expansion] = true;
75
- }
76
- }
77
- // Execute the search
78
- const searchResult = await jiraClient.searchIssues(normalizedArgs.jql, normalizedArgs.startAt, normalizedArgs.maxResults);
79
- // Render to markdown for token efficiency
80
- const markdown = MarkdownRenderer.renderIssueSearchResults(searchResult.issues, searchResult.pagination, normalizedArgs.jql);
81
- return {
82
- content: [
83
- {
84
- type: 'text',
85
- text: markdown,
86
- },
87
- ],
88
- };
89
- }
90
- catch (error) {
91
- console.error('Error in search_jira_issues:', error);
92
- if (error instanceof Error) {
93
- throw new McpError(ErrorCode.InvalidRequest, `Jira API error: ${error.message}`);
94
- }
95
- throw new McpError(ErrorCode.InvalidRequest, 'Failed to execute Jira search');
96
- }
97
- }
98
- default: {
99
- console.error(`Unknown tool requested: ${name}`);
100
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
101
- }
102
- }
103
- }
@@ -1,187 +0,0 @@
1
- import { z } from 'zod';
2
- // Sprint Management Schemas
3
- export const CreateJiraSprintSchema = z.object({
4
- method: z.literal('tools/call'),
5
- params: z.object({
6
- name: z.literal('create_jira_sprint'),
7
- arguments: z.object({
8
- boardId: z.number(),
9
- name: z.string(),
10
- startDate: z.string().optional(),
11
- endDate: z.string().optional(),
12
- goal: z.string().optional(),
13
- }),
14
- }),
15
- });
16
- export const GetJiraSprintSchema = z.object({
17
- method: z.literal('tools/call'),
18
- params: z.object({
19
- name: z.literal('get_jira_sprint'),
20
- arguments: z.object({
21
- sprintId: z.number(),
22
- expand: z.array(z.string()).optional(),
23
- }),
24
- }),
25
- });
26
- export const ListJiraSprintsSchema = z.object({
27
- method: z.literal('tools/call'),
28
- params: z.object({
29
- name: z.literal('list_jira_sprints'),
30
- arguments: z.object({
31
- boardId: z.number(),
32
- state: z.enum(['future', 'active', 'closed']).optional(),
33
- startAt: z.number().optional(),
34
- maxResults: z.number().optional(),
35
- expand: z.array(z.string()).optional(),
36
- }),
37
- }),
38
- });
39
- export const UpdateJiraSprintSchema = z.object({
40
- method: z.literal('tools/call'),
41
- params: z.object({
42
- name: z.literal('update_jira_sprint'),
43
- arguments: z.object({
44
- sprintId: z.number(),
45
- name: z.string().optional(),
46
- goal: z.string().optional(),
47
- startDate: z.string().optional(),
48
- endDate: z.string().optional(),
49
- state: z.enum(['future', 'active', 'closed']).optional(),
50
- }),
51
- }),
52
- });
53
- export const DeleteJiraSprintSchema = z.object({
54
- method: z.literal('tools/call'),
55
- params: z.object({
56
- name: z.literal('delete_jira_sprint'),
57
- arguments: z.object({
58
- sprintId: z.number(),
59
- }),
60
- }),
61
- });
62
- export const UpdateSprintIssuesSchema = z.object({
63
- method: z.literal('tools/call'),
64
- params: z.object({
65
- name: z.literal('update_sprint_issues'),
66
- arguments: z.object({
67
- sprintId: z.number(),
68
- add: z.array(z.string()).optional(),
69
- remove: z.array(z.string()).optional(),
70
- }),
71
- }),
72
- });
73
- // Consolidated API request schemas
74
- export const GetJiraIssueSchema = z.object({
75
- method: z.literal('tools/call'),
76
- params: z.object({
77
- name: z.literal('get_jira_issue'),
78
- arguments: z.object({
79
- issueKey: z.string(),
80
- expand: z.array(z.string()).optional(),
81
- }),
82
- }),
83
- });
84
- export const GetJiraProjectSchema = z.object({
85
- method: z.literal('tools/call'),
86
- params: z.object({
87
- name: z.literal('get_jira_project'),
88
- arguments: z.object({
89
- projectKey: z.string(),
90
- expand: z.array(z.string()).optional(),
91
- include_status_counts: z.boolean().optional(),
92
- }),
93
- }),
94
- });
95
- export const GetJiraBoardSchema = z.object({
96
- method: z.literal('tools/call'),
97
- params: z.object({
98
- name: z.literal('get_jira_board'),
99
- arguments: z.object({
100
- boardId: z.number(),
101
- expand: z.array(z.string()).optional(),
102
- }),
103
- }),
104
- });
105
- export const SearchJiraIssuesSchema = z.object({
106
- method: z.literal('tools/call'),
107
- params: z.object({
108
- name: z.literal('search_jira_issues'),
109
- arguments: z.object({
110
- jql: z.string(),
111
- startAt: z.number().optional(),
112
- maxResults: z.number().optional(),
113
- expand: z.array(z.string()).optional(),
114
- }),
115
- }),
116
- });
117
- export const ListJiraProjectsSchema = z.object({
118
- method: z.literal('tools/call'),
119
- params: z.object({
120
- name: z.literal('list_jira_projects'),
121
- arguments: z.object({
122
- include_status_counts: z.boolean().optional(),
123
- }),
124
- }),
125
- });
126
- export const ListJiraBoardsSchema = z.object({
127
- method: z.literal('tools/call'),
128
- params: z.object({
129
- name: z.literal('list_jira_boards'),
130
- arguments: z.object({
131
- include_sprints: z.boolean().optional(),
132
- }),
133
- }),
134
- });
135
- export const CreateJiraIssueSchema = z.object({
136
- method: z.literal('tools/call'),
137
- params: z.object({
138
- name: z.literal('create_jira_issue'),
139
- arguments: z.object({
140
- projectKey: z.string(),
141
- summary: z.string(),
142
- description: z.string().optional(),
143
- issueType: z.string(),
144
- priority: z.string().optional(),
145
- assignee: z.string().optional(),
146
- labels: z.array(z.string()).optional(),
147
- customFields: z.record(z.any()).optional(),
148
- }),
149
- }),
150
- });
151
- export const UpdateJiraIssueSchema = z.object({
152
- method: z.literal('tools/call'),
153
- params: z.object({
154
- name: z.literal('update_jira_issue'),
155
- arguments: z.object({
156
- issueKey: z.string(),
157
- summary: z.string().optional(),
158
- description: z.string().optional(),
159
- parent: z.union([z.string(), z.null()]).optional(),
160
- assignee: z.string().optional(),
161
- priority: z.string().optional(),
162
- labels: z.array(z.string()).optional(),
163
- customFields: z.record(z.any()).optional(),
164
- }),
165
- }),
166
- });
167
- export const TransitionJiraIssueSchema = z.object({
168
- method: z.literal('tools/call'),
169
- params: z.object({
170
- name: z.literal('transition_jira_issue'),
171
- arguments: z.object({
172
- issueKey: z.string(),
173
- transitionId: z.string(),
174
- comment: z.string().optional(),
175
- }),
176
- }),
177
- });
178
- export const AddJiraCommentSchema = z.object({
179
- method: z.literal('tools/call'),
180
- params: z.object({
181
- name: z.literal('add_jira_comment'),
182
- arguments: z.object({
183
- issueKey: z.string(),
184
- body: z.string(),
185
- }),
186
- }),
187
- });
@@ -1,58 +0,0 @@
1
- /**
2
- * Base formatter for consistent response structure across all entity types
3
- */
4
- export class BaseFormatter {
5
- /**
6
- * Format a response with the standard structure
7
- * @param data The main data for the response
8
- * @param metadata Optional metadata about the response
9
- * @param summary Optional summary information
10
- * @returns A formatted response object
11
- */
12
- static formatResponse(data, metadata, summary) {
13
- return {
14
- data,
15
- ...(metadata && { _metadata: metadata }),
16
- ...(summary && { _summary: summary }),
17
- };
18
- }
19
- /**
20
- * Create metadata for a response
21
- * @param options Metadata options
22
- * @returns Response metadata
23
- */
24
- static createMetadata(options) {
25
- const metadata = {};
26
- if (options.expansions && options.expansions.length > 0) {
27
- metadata.expansions = options.expansions;
28
- }
29
- if (options.related && Object.keys(options.related).length > 0) {
30
- metadata.related = options.related;
31
- }
32
- if (options.pagination) {
33
- const { startAt, maxResults, total } = options.pagination;
34
- metadata.pagination = {
35
- startAt,
36
- maxResults,
37
- total,
38
- hasMore: startAt + maxResults < total,
39
- };
40
- }
41
- return metadata;
42
- }
43
- /**
44
- * Create a summary for a response
45
- * @param options Summary options
46
- * @returns Response summary
47
- */
48
- static createSummary(options) {
49
- const summary = {};
50
- if (options.status_counts && Object.keys(options.status_counts).length > 0) {
51
- summary.status_counts = options.status_counts;
52
- }
53
- if (options.suggested_actions && options.suggested_actions.length > 0) {
54
- summary.suggested_actions = options.suggested_actions;
55
- }
56
- return summary;
57
- }
58
- }
@@ -1,63 +0,0 @@
1
- import { BaseFormatter } from './base-formatter.js';
2
- export class BoardFormatter {
3
- /**
4
- * Format a board response with the standard structure and optional expansions
5
- * @param board The board data
6
- * @param options Expansion options
7
- * @returns A formatted board response
8
- */
9
- static formatBoard(board, options = {}) {
10
- // Create metadata with available expansions
11
- const metadata = this.createBoardMetadata(board, options);
12
- // Create summary
13
- const summary = this.createBoardSummary(board);
14
- return BaseFormatter.formatResponse(board, metadata, summary);
15
- }
16
- /**
17
- * Create metadata for a board response
18
- */
19
- static createBoardMetadata(board, options) {
20
- // Determine which expansions are available but not included
21
- const availableExpansions = [];
22
- if (!options.sprints && !board.sprints) {
23
- availableExpansions.push('sprints');
24
- }
25
- if (!options.issues) {
26
- availableExpansions.push('issues');
27
- }
28
- if (!options.configuration) {
29
- availableExpansions.push('configuration');
30
- }
31
- // Create related entities map
32
- const related = {};
33
- if (board.location?.projectId) {
34
- related.project = board.location.projectName || `Project ${board.location.projectId}`;
35
- }
36
- return BaseFormatter.createMetadata({
37
- expansions: availableExpansions,
38
- related
39
- });
40
- }
41
- /**
42
- * Create a summary for a board response
43
- */
44
- static createBoardSummary(board) {
45
- const suggestedActions = [
46
- {
47
- text: `View all issues on ${board.name}`
48
- }
49
- ];
50
- // Add sprint-related actions if sprints are available
51
- if (board.sprints && board.sprints.length > 0) {
52
- const activeSprints = board.sprints.filter(sprint => sprint.state === 'active');
53
- if (activeSprints.length > 0) {
54
- suggestedActions.push({
55
- text: `View active sprint: ${activeSprints[0].name}`
56
- });
57
- }
58
- }
59
- return BaseFormatter.createSummary({
60
- suggested_actions: suggestedActions
61
- });
62
- }
63
- }
@@ -1,66 +0,0 @@
1
- import { BaseFormatter } from './base-formatter.js';
2
- export class FilterFormatter {
3
- /**
4
- * Format a filter response with the standard structure and optional expansions
5
- * @param filter The filter data
6
- * @param options Expansion options
7
- * @returns A formatted filter response
8
- */
9
- static formatFilter(filter, options = {}) {
10
- // Create metadata with available expansions
11
- const metadata = this.createFilterMetadata(filter, options);
12
- // Create summary
13
- const summary = this.createFilterSummary(filter);
14
- return BaseFormatter.formatResponse(filter, metadata, summary);
15
- }
16
- /**
17
- * Create metadata for a filter response
18
- */
19
- static createFilterMetadata(filter, options) {
20
- // Determine which expansions are available but not included
21
- const availableExpansions = [];
22
- if (!options.jql && !filter.jql) {
23
- availableExpansions.push('jql');
24
- }
25
- if (!options.description && !filter.description) {
26
- availableExpansions.push('description');
27
- }
28
- if (!options.permissions && !filter.sharePermissions) {
29
- availableExpansions.push('permissions');
30
- }
31
- if (!options.issue_count && filter.issueCount === undefined) {
32
- availableExpansions.push('issue_count');
33
- }
34
- // Create related entities map
35
- const related = {
36
- owner: filter.owner
37
- };
38
- return BaseFormatter.createMetadata({
39
- expansions: availableExpansions,
40
- related
41
- });
42
- }
43
- /**
44
- * Create a summary for a filter response
45
- */
46
- static createFilterSummary(filter) {
47
- const suggestedActions = [
48
- {
49
- text: `View filter results: ${filter.name}`
50
- }
51
- ];
52
- if (filter.jql) {
53
- suggestedActions.push({
54
- text: `Edit JQL: ${filter.name}`
55
- });
56
- }
57
- if (!filter.favourite) {
58
- suggestedActions.push({
59
- text: `Add to favorites`
60
- });
61
- }
62
- return BaseFormatter.createSummary({
63
- suggested_actions: suggestedActions
64
- });
65
- }
66
- }
@@ -1,7 +0,0 @@
1
- export * from './base-formatter.js';
2
- export * from './issue-formatter.js';
3
- export * from './project-formatter.js';
4
- export * from './search-formatter.js';
5
- export * from './board-formatter.js';
6
- export * from './sprint-formatter.js';
7
- export * from './filter-formatter.js';
@@ -1,84 +0,0 @@
1
- import { BaseFormatter } from './base-formatter.js';
2
- export class IssueFormatter {
3
- /**
4
- * Format an issue response with the standard structure and optional expansions
5
- * @param issue The issue data
6
- * @param options Expansion options
7
- * @param transitions Optional transitions data (if requested)
8
- * @returns A formatted issue response
9
- */
10
- static formatIssue(issue, options = {}, transitions) {
11
- // Create metadata with available expansions
12
- const metadata = this.createIssueMetadata(issue, options, transitions);
13
- // Create summary with status and suggested actions
14
- const summary = this.createIssueSummary(issue, transitions);
15
- return BaseFormatter.formatResponse(issue, metadata, summary);
16
- }
17
- /**
18
- * Create metadata for an issue response
19
- */
20
- static createIssueMetadata(issue, options, transitions) {
21
- // Determine which expansions are available but not included
22
- const availableExpansions = [];
23
- if (!options.comments && issue.comments === undefined) {
24
- availableExpansions.push('comments');
25
- }
26
- if (!options.transitions && transitions === undefined) {
27
- availableExpansions.push('transitions');
28
- }
29
- if (!options.attachments && issue.attachments === undefined) {
30
- availableExpansions.push('attachments');
31
- }
32
- if (!options.related_issues) {
33
- availableExpansions.push('related_issues');
34
- }
35
- if (!options.history) {
36
- availableExpansions.push('history');
37
- }
38
- // Create related entities map
39
- const related = {};
40
- if (issue.parent) {
41
- related.parent = issue.parent;
42
- }
43
- // Extract related issues from issue links
44
- const relatedIssues = issue.issueLinks
45
- .map(link => link.outward || link.inward)
46
- .filter((key) => key !== null);
47
- if (relatedIssues.length > 0) {
48
- related.linked_issues = relatedIssues;
49
- }
50
- return BaseFormatter.createMetadata({
51
- expansions: availableExpansions,
52
- related
53
- });
54
- }
55
- /**
56
- * Create a summary for an issue response
57
- */
58
- static createIssueSummary(issue, transitions) {
59
- const suggestedActions = [];
60
- // Add suggested actions based on available transitions
61
- if (transitions && transitions.length > 0) {
62
- // Common transitions to suggest
63
- const commonTransitions = ['Done', 'In Progress', 'To Do', 'Closed', 'Resolved'];
64
- for (const transitionName of commonTransitions) {
65
- const transition = transitions.find(t => t.name === transitionName);
66
- if (transition) {
67
- suggestedActions.push({
68
- text: `Move to ${transitionName}`,
69
- action_id: transition.id
70
- });
71
- }
72
- }
73
- }
74
- // Add assignment suggestion if not assigned
75
- if (!issue.assignee) {
76
- suggestedActions.push({
77
- text: 'Assign to team member'
78
- });
79
- }
80
- return BaseFormatter.createSummary({
81
- suggested_actions: suggestedActions
82
- });
83
- }
84
- }
@@ -1,55 +0,0 @@
1
- import { BaseFormatter } from './base-formatter.js';
2
- export class ProjectFormatter {
3
- /**
4
- * Format a project response with the standard structure and optional expansions
5
- * @param project The project data
6
- * @param options Expansion options
7
- * @returns A formatted project response
8
- */
9
- static formatProject(project, options = {}) {
10
- // Create metadata with available expansions
11
- const metadata = this.createProjectMetadata(project, options);
12
- // Create summary with status counts
13
- const summary = this.createProjectSummary(project);
14
- return BaseFormatter.formatResponse(project, metadata, summary);
15
- }
16
- /**
17
- * Create metadata for a project response
18
- */
19
- static createProjectMetadata(project, options) {
20
- // Determine which expansions are available but not included
21
- const availableExpansions = [];
22
- if (!options.boards) {
23
- availableExpansions.push('boards');
24
- }
25
- if (!options.components) {
26
- availableExpansions.push('components');
27
- }
28
- if (!options.versions) {
29
- availableExpansions.push('versions');
30
- }
31
- if (!options.recent_issues) {
32
- availableExpansions.push('recent_issues');
33
- }
34
- return BaseFormatter.createMetadata({
35
- expansions: availableExpansions
36
- });
37
- }
38
- /**
39
- * Create a summary for a project response
40
- */
41
- static createProjectSummary(project) {
42
- const suggestedActions = [
43
- {
44
- text: `View all issues in ${project.key}`
45
- },
46
- {
47
- text: `Create issue in ${project.key}`
48
- }
49
- ];
50
- return BaseFormatter.createSummary({
51
- status_counts: project.status_counts,
52
- suggested_actions: suggestedActions
53
- });
54
- }
55
- }
@@ -1,62 +0,0 @@
1
- import { BaseFormatter } from './base-formatter.js';
2
- export class SearchFormatter {
3
- /**
4
- * Format a search response with the standard structure and optional expansions
5
- * @param searchResult The search result data
6
- * @param options Expansion options
7
- * @returns A formatted search response
8
- */
9
- static formatSearchResult(searchResult, options = {}) {
10
- // Create metadata with available expansions and pagination
11
- const metadata = this.createSearchMetadata(searchResult, options);
12
- // Create summary with status counts
13
- const summary = this.createSearchSummary(searchResult);
14
- return BaseFormatter.formatResponse(searchResult, metadata, summary);
15
- }
16
- /**
17
- * Create metadata for a search response
18
- */
19
- static createSearchMetadata(searchResult, options) {
20
- // Determine which expansions are available but not included
21
- const availableExpansions = [];
22
- if (!options.issue_details) {
23
- availableExpansions.push('issue_details');
24
- }
25
- if (!options.transitions) {
26
- availableExpansions.push('transitions');
27
- }
28
- if (!options.comments_preview) {
29
- availableExpansions.push('comments_preview');
30
- }
31
- return BaseFormatter.createMetadata({
32
- expansions: availableExpansions,
33
- pagination: {
34
- startAt: searchResult.pagination.startAt,
35
- maxResults: searchResult.pagination.maxResults,
36
- total: searchResult.pagination.total
37
- }
38
- });
39
- }
40
- /**
41
- * Create a summary for a search response
42
- */
43
- static createSearchSummary(searchResult) {
44
- // Count issues by status
45
- const statusCounts = {};
46
- for (const issue of searchResult.issues) {
47
- const status = issue.status;
48
- statusCounts[status] = (statusCounts[status] || 0) + 1;
49
- }
50
- const suggestedActions = [];
51
- // Add pagination actions if there are more results
52
- if (searchResult.pagination.hasMore) {
53
- suggestedActions.push({
54
- text: 'Load more results'
55
- });
56
- }
57
- return BaseFormatter.createSummary({
58
- status_counts: statusCounts,
59
- suggested_actions: suggestedActions
60
- });
61
- }
62
- }
@@ -1,111 +0,0 @@
1
- import { BaseFormatter } from './base-formatter.js';
2
- export class SprintFormatter {
3
- /**
4
- * Format a sprint response with the standard structure and optional expansions
5
- * @param sprint The sprint data
6
- * @param options Expansion options
7
- * @returns A formatted sprint response
8
- */
9
- static formatSprint(sprint, options = {}) {
10
- // Create metadata with available expansions
11
- const metadata = this.createSprintMetadata(sprint, options);
12
- // Create summary
13
- const summary = this.createSprintSummary(sprint);
14
- return BaseFormatter.formatResponse(sprint, metadata, summary);
15
- }
16
- /**
17
- * Format a list of sprints
18
- * @param sprints Array of sprint data
19
- * @param pagination Pagination information
20
- * @returns A formatted response with sprints array
21
- */
22
- static formatSprintList(sprints, pagination) {
23
- // Create metadata
24
- const metadata = {};
25
- if (pagination) {
26
- metadata.pagination = {
27
- startAt: pagination.startAt,
28
- maxResults: pagination.maxResults,
29
- total: pagination.total,
30
- hasMore: pagination.startAt + pagination.maxResults < pagination.total,
31
- };
32
- }
33
- // Create summary with status counts
34
- const statusCounts = {
35
- future: 0,
36
- active: 0,
37
- closed: 0,
38
- };
39
- sprints.forEach(sprint => {
40
- if (sprint.state && statusCounts[sprint.state] !== undefined) {
41
- statusCounts[sprint.state]++;
42
- }
43
- });
44
- const summary = {
45
- status_counts: statusCounts,
46
- suggested_actions: [
47
- { text: 'Create new sprint', action_id: 'create_sprint' },
48
- ],
49
- };
50
- return BaseFormatter.formatResponse(sprints, metadata, summary);
51
- }
52
- /**
53
- * Create metadata for a sprint response
54
- */
55
- static createSprintMetadata(sprint, options) {
56
- // Determine which expansions are available but not included
57
- const availableExpansions = [];
58
- if (!options.issues && !sprint.issues) {
59
- availableExpansions.push('issues');
60
- }
61
- if (!options.report && !sprint.report) {
62
- availableExpansions.push('report');
63
- }
64
- // Create related entities map
65
- const related = {
66
- board: sprint.boardId.toString(),
67
- };
68
- return BaseFormatter.createMetadata({
69
- expansions: availableExpansions,
70
- related
71
- });
72
- }
73
- /**
74
- * Create a summary for a sprint response
75
- */
76
- static createSprintSummary(sprint) {
77
- const suggestedActions = [];
78
- // Add suggested actions based on sprint state
79
- switch (sprint.state) {
80
- case 'future':
81
- suggestedActions.push({ text: 'Start Sprint', action_id: 'start_sprint' });
82
- suggestedActions.push({ text: 'Add Issues to Sprint', action_id: 'add_issues' });
83
- suggestedActions.push({ text: 'Edit Sprint', action_id: 'update_sprint' });
84
- break;
85
- case 'active':
86
- suggestedActions.push({ text: 'Complete Sprint', action_id: 'complete_sprint' });
87
- suggestedActions.push({ text: 'Add Issues to Sprint', action_id: 'add_issues' });
88
- suggestedActions.push({ text: 'Remove Issues from Sprint', action_id: 'remove_issues' });
89
- suggestedActions.push({ text: 'Edit Sprint', action_id: 'update_sprint' });
90
- break;
91
- case 'closed':
92
- suggestedActions.push({ text: 'View Sprint Report', action_id: 'view_report' });
93
- suggestedActions.push({ text: 'Create New Sprint', action_id: 'create_sprint' });
94
- break;
95
- }
96
- // Add status counts if issues are available
97
- const statusCounts = {};
98
- if (sprint.issues && sprint.issues.length > 0) {
99
- sprint.issues.forEach(issue => {
100
- if (!statusCounts[issue.status]) {
101
- statusCounts[issue.status] = 0;
102
- }
103
- statusCounts[issue.status]++;
104
- });
105
- }
106
- return BaseFormatter.createSummary({
107
- status_counts: Object.keys(statusCounts).length > 0 ? statusCounts : undefined,
108
- suggested_actions: suggestedActions
109
- });
110
- }
111
- }