Skip to content

Commit 6a4783c

Browse files
committed
feat: consolidate output into ControllerResponse.content string
1 parent ee69697 commit 6a4783c

5 files changed

Lines changed: 100 additions & 123 deletions

src/controllers/atlassian.issues.controller.test.ts

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import atlassianIssuesController from './atlassian.issues.controller.js';
22
import { config } from '../utils/config.util.js';
33
import { getAtlassianCredentials } from '../utils/transport.util.js';
44
import { McpError } from '../utils/error.util.js';
5-
import { formatSeparator, formatDate } from '../utils/formatter.util.js';
5+
import { formatSeparator } from '../utils/formatter.util.js';
66

77
describe('Atlassian Issues Controller', () => {
88
// Load configuration and check for credentials before running tests
@@ -31,25 +31,10 @@ describe('Atlassian Issues Controller', () => {
3131
expect(result).toBeDefined();
3232
expect(result).toHaveProperty('content');
3333
expect(typeof result.content).toBe('string');
34-
expect(result).toHaveProperty('pagination');
35-
36-
// Check pagination object structure and basic types
37-
expect(result.pagination).toBeDefined();
38-
expect(result.pagination).toHaveProperty('hasMore');
39-
expect(typeof result.pagination?.hasMore).toBe('boolean');
40-
expect(result.pagination).toHaveProperty('count');
41-
expect(typeof result.pagination?.count).toBe('number');
42-
expect(result.pagination).toHaveProperty('total');
43-
expect(typeof result.pagination?.total).toBe('number');
44-
if (result.pagination?.hasMore) {
45-
expect(result.pagination).toHaveProperty('nextCursor');
46-
expect(typeof result.pagination?.nextCursor).toBe('string');
47-
}
4834

4935
// Check that content does NOT contain pagination string
50-
expect(result.content).not.toContain('Showing');
51-
expect(result.content).not.toContain('Next StartAt');
52-
expect(result.content).not.toContain(`total issues`);
36+
expect(result.content).toContain('Showing');
37+
expect(result.content).toContain('total items');
5338

5439
// Check basic markdown content - check for expected formatting from live data
5540
if (result.content !== 'No issues found matching your criteria.') {
@@ -71,12 +56,12 @@ describe('Atlassian Issues Controller', () => {
7156
expect(result).toBeDefined();
7257
expect(typeof result.content).toBe('string');
7358

74-
// Check if pagination is handled correctly
75-
if (result.pagination && result.pagination.hasMore) {
76-
expect(result.pagination.nextCursor).toBeDefined();
77-
expect(
78-
parseInt(result.pagination.nextCursor as string, 10),
79-
).toBe(10);
59+
// Pagination info should now be in the content
60+
expect(result.content).toContain('JQL Query:');
61+
62+
// Check if content contains pagination information
63+
if (result.content.includes('More results are available')) {
64+
expect(result.content).toContain('Use --start-at');
8065
}
8166
}, 30000);
8267

@@ -108,7 +93,6 @@ describe('Atlassian Issues Controller', () => {
10893
expect(result).toBeDefined();
10994
expect(typeof result.content).toBe('string');
11095
expect(result.content).toContain('# Jira Issues');
111-
expect(result.pagination).toBeDefined();
11296
} catch (error) {
11397
// Some Jira instances may not support all JQL terms or fields
11498
// So we'll accept errors that are properly formatted
@@ -130,21 +114,16 @@ describe('Atlassian Issues Controller', () => {
130114
expect(typeof result.content).toBe('string');
131115

132116
// Check for appropriate message for no results (actual formatter output)
133-
expect(result.content).toBe(
134-
'No issues found.\n\n' +
135-
formatSeparator() +
136-
'\n' +
137-
`*Information retrieved at: ${formatDate(new Date())}*`,
138-
);
139-
expect(result.pagination).toBeDefined();
140-
expect(result.pagination?.hasMore).toBe(false);
141-
expect(result.pagination?.count).toBe(0);
142-
expect(result.pagination?.total).toBe(0);
143-
expect(result.pagination?.nextCursor).toBeUndefined();
117+
expect(result.content).toContain('No issues found.');
118+
119+
// Content should include the JQL query and formatted timestamp
120+
expect(result.content).toContain(`summary ~ "${uniqueTerm}"`);
121+
expect(result.content).toContain(formatSeparator());
122+
expect(result.content).toContain('Information retrieved at:');
144123

145124
// Check that content does NOT contain pagination string
146-
expect(result.content).not.toContain('Showing');
147-
expect(result.content).not.toContain('Next StartAt');
125+
expect(result.content).toContain('Showing');
126+
expect(result.content).toContain('total items');
148127
}, 30000);
149128

150129
it('should handle error for invalid JQL', async () => {

src/controllers/atlassian.issues.controller.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
DevInfoResponse,
2828
DevInfoSummaryResponse,
2929
} from '../services/vendor.atlassian.issues.types.js';
30+
import { formatPagination } from '../utils/formatter.util.js';
3031

3132
/**
3233
* Controller for managing Jira issues.
@@ -188,9 +189,24 @@ async function list(
188189

189190
const formattedIssues = formatIssuesList(formatterInput);
190191

192+
// Create JQL info section
193+
const jqlInfoText = `**Executed JQL Query:** \`${finalJql}\``;
194+
195+
// Combine JQL info and issues content
196+
const finalContent = jqlInfoText + '\n\n' + formattedIssues;
197+
198+
// Combine formatted content with pagination information
199+
let contentWithPagination = finalContent;
200+
if (
201+
pagination &&
202+
(pagination.hasMore || pagination.count !== undefined)
203+
) {
204+
const paginationString = formatPagination(pagination);
205+
contentWithPagination += '\n\n' + paginationString;
206+
}
207+
191208
return {
192-
content: formattedIssues,
193-
pagination,
209+
content: contentWithPagination,
194210
};
195211
} catch (error) {
196212
throw handleControllerError(

src/controllers/atlassian.projects.controller.test.ts

Lines changed: 46 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import atlassianProjectsController from './atlassian.projects.controller.js';
22
import { getAtlassianCredentials } from '../utils/transport.util.js';
33
import { config } from '../utils/config.util.js';
44
import { McpError } from '../utils/error.util.js'; // Import McpError
5-
import { formatSeparator, formatDate } from '../utils/formatter.util.js'; // Add imports
5+
import { formatSeparator } from '../utils/formatter.util.js'; // Add imports
66

77
describe('Atlassian Projects Controller', () => {
88
// Load configuration and check for credentials before all tests
@@ -29,25 +29,10 @@ describe('Atlassian Projects Controller', () => {
2929
expect(result).toBeDefined();
3030
expect(result).toHaveProperty('content');
3131
expect(typeof result.content).toBe('string');
32-
expect(result).toHaveProperty('pagination');
33-
34-
// Check pagination object structure and basic types
35-
expect(result.pagination).toBeDefined();
36-
expect(result.pagination).toHaveProperty('hasMore');
37-
expect(typeof result.pagination?.hasMore).toBe('boolean');
38-
expect(result.pagination).toHaveProperty('count');
39-
expect(typeof result.pagination?.count).toBe('number');
40-
expect(result.pagination).toHaveProperty('total');
41-
expect(typeof result.pagination?.total).toBe('number');
42-
if (result.pagination?.hasMore) {
43-
expect(result.pagination).toHaveProperty('nextCursor');
44-
expect(typeof result.pagination?.nextCursor).toBe('string');
45-
}
4632

4733
// Check that content does NOT contain pagination string anymore
48-
expect(result.content).not.toContain('Showing');
49-
expect(result.content).not.toContain('Next StartAt');
50-
expect(result.content).not.toContain(`total items`);
34+
expect(result.content).toContain('Showing');
35+
expect(result.content).toContain('total items');
5136

5237
// Basic Markdown content checks - check for expected formatting from live data
5338
if (
@@ -67,31 +52,31 @@ describe('Atlassian Projects Controller', () => {
6752
const result1 = await atlassianProjectsController.list({
6853
limit: 1,
6954
});
70-
expect(result1.pagination?.count).toBeLessThanOrEqual(1);
71-
72-
// If there's a next page, fetch it
73-
if (result1.pagination?.hasMore && result1.pagination.nextCursor) {
74-
// Parse the nextCursor (which is the next startAt value as a string)
75-
const nextStartAt = parseInt(result1.pagination.nextCursor, 10);
76-
if (isNaN(nextStartAt)) {
77-
throw new Error(
78-
`Invalid nextCursor format for startAt: ${result1.pagination.nextCursor}`,
79-
);
80-
}
81-
const result2 = await atlassianProjectsController.list({
82-
limit: 1,
83-
// Pass the parsed number as startAt
84-
startAt: nextStartAt,
85-
});
86-
expect(result2.pagination?.count).toBeLessThanOrEqual(1);
87-
// Check if content is different (simple check)
88-
if (
89-
result1.content !==
90-
'No Jira projects found matching your criteria.' &&
91-
result2.content !==
92-
'No Jira projects found matching your criteria.'
93-
) {
94-
expect(result1.content).not.toEqual(result2.content);
55+
56+
// Check if content contains pagination information
57+
if (result1.content.includes('More results are available')) {
58+
// Get the next startAt value from the content
59+
const startAtMatch = result1.content.match(
60+
/Use --start-at (\d+) to view more/,
61+
);
62+
if (startAtMatch && startAtMatch[1]) {
63+
const nextStartAt = parseInt(startAtMatch[1], 10);
64+
65+
// Fetch second page
66+
const result2 = await atlassianProjectsController.list({
67+
limit: 1,
68+
startAt: nextStartAt,
69+
});
70+
71+
// If both results have content, they should be different
72+
if (
73+
result1.content !==
74+
'No Jira projects found matching your criteria.' &&
75+
result2.content !==
76+
'No Jira projects found matching your criteria.'
77+
) {
78+
expect(result1.content).not.toEqual(result2.content);
79+
}
9580
}
9681
} else {
9782
console.warn(
@@ -131,13 +116,14 @@ describe('Atlassian Projects Controller', () => {
131116
limit: 5,
132117
});
133118

134-
expect(result.pagination?.count).toBeLessThanOrEqual(5);
119+
// Check if the content is well-formed
135120
if (
136121
result.content !==
137122
'No Jira projects found matching your criteria.'
138123
) {
139124
expect(result.content).toMatch(/^# Jira Projects/m);
140-
// Further checks could involve verifying sorting/filtering in Markdown, but that's complex
125+
// Content should contain pagination information
126+
expect(result.content).toContain('Information retrieved at:');
141127
}
142128
}, 30000);
143129

@@ -155,21 +141,15 @@ describe('Atlassian Projects Controller', () => {
155141
expect(typeof result.content).toBe('string');
156142

157143
// Check specific empty result message including the standard footer
158-
expect(result.content).toBe(
159-
'No Jira projects found matching your criteria.\n\n' +
160-
formatSeparator() +
161-
'\n' +
162-
`*Information retrieved at: ${formatDate(new Date())}*`,
144+
expect(result.content).toContain(
145+
'No Jira projects found matching your criteria',
163146
);
164-
// Verify pagination properties for empty result
165-
expect(result.pagination).toHaveProperty('count', 0);
166-
expect(result.pagination?.hasMore).toBe(false);
167-
expect(result.pagination?.total).toBe(0);
168-
expect(result.pagination?.nextCursor).toBeUndefined();
147+
expect(result.content).toContain(formatSeparator());
148+
expect(result.content).toContain('Information retrieved at:');
169149

170150
// Check that content does NOT contain pagination string
171-
expect(result.content).not.toContain('Showing');
172-
expect(result.content).not.toContain('Next StartAt');
151+
expect(result.content).toContain('Showing');
152+
expect(result.content).toContain('total items');
173153
}, 30000);
174154

175155
it('should handle various filtering combinations', async () => {
@@ -211,11 +191,9 @@ describe('Atlassian Projects Controller', () => {
211191
// Verify response (might find 0 or 1 projects due to exact matching)
212192
expect(result).toHaveProperty('content');
213193
expect(typeof result.content).toBe('string');
214-
expect(result).toHaveProperty('pagination');
215-
expect(result.pagination).toHaveProperty('count');
216194

217195
// If we found exactly the project we filtered for, its key should be in the content
218-
if (result.pagination?.count === 1) {
196+
if (result.content.includes(projectKey)) {
219197
expect(result.content).toContain(`**Key**: ${projectKey}`);
220198
}
221199
}, 30000);
@@ -231,21 +209,16 @@ describe('Atlassian Projects Controller', () => {
231209
limit: 10,
232210
});
233211

234-
expect(result.content).toBe(
235-
'No Jira projects found matching your criteria.\n\n' +
236-
formatSeparator() +
237-
'\n' +
238-
`*Information retrieved at: ${formatDate(new Date())}*`,
239-
); // Check actual empty message
240-
expect(result.pagination).toBeDefined();
241-
expect(result.pagination?.hasMore).toBe(false);
242-
expect(result.pagination?.count).toBe(0);
243-
expect(result.pagination?.total).toBe(0);
244-
expect(result.pagination?.nextCursor).toBeUndefined();
212+
// Check actual empty message and formatting
213+
expect(result.content).toContain(
214+
'No Jira projects found matching your criteria',
215+
);
216+
expect(result.content).toContain(formatSeparator());
217+
expect(result.content).toContain('Information retrieved at:');
245218

246219
// Check that content does NOT contain pagination string anymore
247-
expect(result.content).not.toContain('Showing');
248-
expect(result.content).not.toContain('Next StartAt');
220+
expect(result.content).toContain('Showing');
221+
expect(result.content).toContain('total items');
249222
}, 30000);
250223
});
251224

@@ -297,7 +270,6 @@ describe('Atlassian Projects Controller', () => {
297270
// Verify the ControllerResponse structure
298271
expect(result).toHaveProperty('content');
299272
expect(typeof result.content).toBe('string');
300-
expect(result).not.toHaveProperty('pagination'); // 'get' shouldn't have pagination
301273

302274
// Verify Markdown content
303275
expect(result.content).toMatch(/^# Project:/m); // Main heading for project details

src/controllers/atlassian.projects.controller.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
formatProjectsList,
1616
formatProjectDetails,
1717
} from './atlassian.projects.formatter.js';
18+
import { formatPagination } from '../utils/formatter.util.js';
1819
import {
1920
DEFAULT_PAGE_SIZE,
2021
applyDefaults,
@@ -96,9 +97,18 @@ async function list(
9697
// Format the projects data for display using the formatter
9798
const formattedProjects = formatProjectsList(projectsData);
9899

100+
// Combine formatted content with pagination information
101+
let finalContent = formattedProjects;
102+
if (
103+
pagination &&
104+
(pagination.hasMore || pagination.count !== undefined)
105+
) {
106+
const paginationString = formatPagination(pagination);
107+
finalContent += '\n\n' + paginationString;
108+
}
109+
99110
return {
100-
content: formattedProjects,
101-
pagination,
111+
content: finalContent,
102112
};
103113
} catch (error) {
104114
// Use throw instead of return

src/types/common.types.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/**
88
* Common pagination information for API responses.
99
* This is used for providing consistent pagination details to clients.
10+
* Note: This is now only used internally by controllers and formatPagination.
1011
*/
1112
export interface ResponsePagination {
1213
/**
@@ -37,17 +38,16 @@ export interface ResponsePagination {
3738
/**
3839
* Common response structure for controller operations.
3940
* All controller methods should return this structure.
41+
*
42+
* All output, including pagination information and metadata, is now consolidated
43+
* into the content field as a single Markdown-formatted string.
4044
*/
4145
export interface ControllerResponse {
4246
/**
4347
* Formatted content to be displayed to the user.
44-
* Usually a Markdown-formatted string.
48+
* This is a Markdown-formatted string that includes all information
49+
* (main content, pagination details, and any metadata) that needs
50+
* to be presented to the user.
4551
*/
4652
content: string;
47-
48-
/**
49-
* Optional pagination information for list operations.
50-
* If present, indicates that more results are available.
51-
*/
52-
pagination?: ResponsePagination;
5353
}

0 commit comments

Comments
 (0)