Skip to content

Commit 4384780

Browse files
authored
feat: modernize dependencies and ensure Zod v3.25.76 MCP SDK compatibility (#89)
* feat: modernize dependencies and migrate to Zod v4 - Update all dependencies to latest versions - Migrate from Zod v3 to v4.1.5 - Update error handling from .errors to .issues for Zod v4 compatibility - Fix handler signatures for MCP SDK compatibility - Update z.record() calls to include both key and value types - Fix validation utility to use simplified Zod v4 types - Update test expectations for API status codes (400 -> 410) - Ensure build passes with all TypeScript fixes * fix(deps): downgrade Zod to v3.25.76 for MCP SDK compatibility - Resolve MCP server startup error: keyValidator._parse is not a function - MCP SDK v1.17.5 requires Zod v3, project was using v4.1.5 - Successfully tested HTTP streamable transport on port 3031 - All MCP tools verified working with live Jira API integration - Comprehensive curl testing completed for read-only operations * fix: update package-lock.json with zod v3.25.76 compatibility - Reverted from zod v4.1.5 to v3.25.76 for MCP SDK compatibility - Fixed keyValidator._parse errors in MCP tool registrations - Verified all tools working with proper .shape references - Completed clean reinstall with full test verification
1 parent d770483 commit 4384780

17 files changed

Lines changed: 646 additions & 577 deletions

package-lock.json

Lines changed: 446 additions & 443 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,43 +61,43 @@
6161
"author": "",
6262
"license": "ISC",
6363
"devDependencies": {
64-
"@eslint/js": "^9.33.0",
64+
"@eslint/js": "^9.35.0",
6565
"@semantic-release/changelog": "^6.0.3",
6666
"@semantic-release/exec": "^7.1.0",
6767
"@semantic-release/git": "^10.0.1",
68-
"@semantic-release/github": "^11.0.4",
68+
"@semantic-release/github": "^11.0.5",
6969
"@semantic-release/npm": "^12.0.2",
7070
"@types/cors": "^2.8.19",
7171
"@types/express": "^5.0.3",
7272
"@types/jest": "^30.0.0",
73-
"@types/node": "^24.3.0",
73+
"@types/node": "^24.3.1",
7474
"@types/turndown": "^5.0.5",
75-
"@typescript-eslint/eslint-plugin": "^8.39.1",
76-
"@typescript-eslint/parser": "^8.39.1",
77-
"eslint": "^9.33.0",
75+
"@typescript-eslint/eslint-plugin": "^8.43.0",
76+
"@typescript-eslint/parser": "^8.43.0",
77+
"eslint": "^9.35.0",
7878
"eslint-config-prettier": "^10.1.8",
7979
"eslint-plugin-filenames": "^1.3.2",
8080
"eslint-plugin-prettier": "^5.5.4",
81-
"jest": "^30.0.5",
81+
"jest": "^30.1.3",
8282
"node-fetch": "^3.3.2",
8383
"nodemon": "^3.1.10",
84-
"npm-check-updates": "^18.0.2",
84+
"npm-check-updates": "^18.1.0",
8585
"prettier": "^3.6.2",
8686
"semantic-release": "^24.2.7",
8787
"ts-jest": "^29.4.1",
8888
"ts-node": "^10.9.2",
8989
"typescript": "^5.9.2",
90-
"typescript-eslint": "^8.39.1"
90+
"typescript-eslint": "^8.43.0"
9191
},
9292
"publishConfig": {
9393
"registry": "https://registry.npmjs.org/",
9494
"access": "public"
9595
},
9696
"dependencies": {
97-
"@modelcontextprotocol/sdk": "^1.17.3",
97+
"@modelcontextprotocol/sdk": "^1.17.5",
9898
"commander": "^14.0.0",
9999
"cors": "^2.8.5",
100-
"dotenv": "^17.2.1",
100+
"dotenv": "^17.2.2",
101101
"express": "^5.1.0",
102102
"turndown": "^7.2.1",
103103
"zod": "^3.25.76"

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ describe('Atlassian Issues Controller', () => {
3232
expect(result).toHaveProperty('content');
3333
expect(typeof result.content).toBe('string');
3434

35-
// Check that content does NOT contain pagination string
35+
// Check that content contains pagination information
3636
expect(result.content).toContain('Showing');
37-
expect(result.content).toContain('total items');
37+
// New API doesn't provide total count, so check for either format
38+
const hasLegacyFormat = result.content.includes('total items');
39+
const hasNewFormat =
40+
result.content.includes('items.') &&
41+
!result.content.includes('total items');
42+
expect(hasLegacyFormat || hasNewFormat).toBe(true);
3843

3944
// Check basic markdown content - check for expected formatting from live data
4045
if (result.content !== 'No issues found matching your criteria.') {
@@ -121,9 +126,14 @@ describe('Atlassian Issues Controller', () => {
121126
expect(result.content).toContain(formatSeparator());
122127
expect(result.content).toContain('Information retrieved at:');
123128

124-
// Check that content does NOT contain pagination string
129+
// Check that content contains pagination information
125130
expect(result.content).toContain('Showing');
126-
expect(result.content).toContain('total items');
131+
// New API doesn't provide total count, so check for either format
132+
const hasLegacyFormat = result.content.includes('total items');
133+
const hasNewFormat =
134+
result.content.includes('items.') &&
135+
!result.content.includes('total items');
136+
expect(hasLegacyFormat || hasNewFormat).toBe(true);
127137
}, 30000);
128138

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

src/controllers/atlassian.issues.controller.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ async function list(
128128

129129
let finalJql = jqlParts.join(' AND ');
130130

131+
// Handle the case where no search criteria are provided
132+
if (finalJql.trim() === '') {
133+
// Default search if no criteria provided - must be bounded for new API
134+
// Use a reasonable time window to ensure the query is bounded
135+
finalJql = 'updated >= -90d';
136+
}
137+
138+
// Apply ordering logic only after ensuring we have a base query
131139
if (mergedOptions.orderBy) {
132140
if (!finalJql.toUpperCase().includes('ORDER BY')) {
133141
finalJql += ` ORDER BY ${mergedOptions.orderBy}`;
@@ -144,11 +152,6 @@ async function list(
144152
finalJql += ' ORDER BY updated DESC';
145153
}
146154

147-
if (finalJql.trim() === '') {
148-
// Default search if no criteria provided - maybe just order by updated?
149-
finalJql = 'ORDER BY updated DESC';
150-
}
151-
152155
methodLogger.debug(`Executing generated JQL: ${finalJql}`);
153156

154157
const params: SearchIssuesParams = {
@@ -162,7 +165,7 @@ async function list(
162165
const issuesData = await atlassianIssuesService.search(params);
163166

164167
methodLogger.debug(
165-
`Retrieved ${issuesData.issues.length} issues out of ${issuesData.total} total`,
168+
`Retrieved ${issuesData.issues.length} issues${issuesData.isLast ? ' (final page)' : ' (more available)'}`,
166169
);
167170

168171
const pagination = extractPaginationInfo(

src/services/vendor.atlassian.issues.service.ts

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,14 @@ const API_PATH = '/rest/api/3';
9494
* Search for Jira issues using JQL and other criteria
9595
*
9696
* Retrieves a list of issues from Jira based on JQL query and other
97-
* search parameters. Supports pagination, field selection, and expansion.
97+
* search parameters. Uses the new enhanced JQL search API endpoint.
98+
* Supports pagination with nextPageToken, field selection, and expansion.
9899
*
99100
* @async
100101
* @memberof VendorAtlassianIssuesService
101102
* @param {SearchIssuesParams} [params={}] - Optional parameters for customizing the search
102103
* @param {string} [params.jql] - JQL query string for filtering issues
103-
* @param {number} [params.startAt] - Pagination start index
104+
* @param {number} [params.startAt] - Pagination start index (converted to nextPageToken internally)
104105
* @param {number} [params.maxResults] - Maximum number of results to return
105106
* @param {string[]} [params.fields] - Issue fields to include in response
106107
* @param {string[]} [params.expand] - Issue data to expand in response
@@ -134,54 +135,55 @@ async function search(
134135
);
135136
}
136137

137-
// Build query parameters
138-
const queryParams = new URLSearchParams();
138+
// Use the new enhanced JQL search API endpoint
139+
const path = `${API_PATH}/search/jql`;
140+
141+
// Build request body for POST request to new endpoint
142+
const requestBody: Record<string, unknown> = {};
139143

140-
// JQL and validation
144+
// JQL is required for the new endpoint
141145
if (params.jql) {
142-
queryParams.set('jql', params.jql);
143-
}
144-
if (params.validateQuery !== undefined) {
145-
queryParams.set('validateQuery', params.validateQuery.toString());
146+
requestBody.jql = params.jql;
146147
}
147148

148-
// Pagination
149-
if (params.startAt !== undefined) {
150-
queryParams.set('startAt', params.startAt.toString());
149+
// Pagination - prefer nextPageToken over startAt
150+
if (params.nextPageToken) {
151+
requestBody.nextPageToken = params.nextPageToken;
151152
}
152153
if (params.maxResults !== undefined) {
153-
queryParams.set('maxResults', params.maxResults.toString());
154-
}
155-
if (params.nextPageToken) {
156-
queryParams.set('nextPageToken', params.nextPageToken);
154+
requestBody.maxResults = params.maxResults;
157155
}
158156

159157
// Field selection and expansion
158+
// If no fields are specified, request the standard fields for backward compatibility
160159
if (params.fields?.length) {
161-
queryParams.set('fields', params.fields.join(','));
160+
requestBody.fields = params.fields;
161+
} else {
162+
// Default fields to maintain backward compatibility with old API
163+
requestBody.fields = ['*all'];
162164
}
165+
163166
if (params.expand?.length) {
164-
queryParams.set('expand', params.expand.join(','));
167+
requestBody.expand = params.expand.join(',');
165168
}
166169
if (params.properties?.length) {
167-
queryParams.set('properties', params.properties.join(','));
170+
requestBody.properties = params.properties;
168171
}
169172
if (params.fieldsByKeys !== undefined) {
170-
queryParams.set('fieldsByKeys', params.fieldsByKeys.toString());
173+
requestBody.fieldsByKeys = params.fieldsByKeys;
171174
}
172175
if (params.reconcileIssues !== undefined) {
173-
queryParams.set('reconcileIssues', params.reconcileIssues.toString());
176+
requestBody.reconcileIssues = params.reconcileIssues;
174177
}
175178

176-
const queryString = queryParams.toString()
177-
? `?${queryParams.toString()}`
178-
: '';
179-
const path = `${API_PATH}/search${queryString}`;
180-
181-
methodLogger.debug(`Calling Jira API: ${path}`);
179+
methodLogger.debug(`Calling new Jira JQL API: ${path}`);
180+
methodLogger.debug('Request body:', requestBody);
182181

183182
try {
184-
const rawData = await fetchAtlassian(credentials, path);
183+
const rawData = await fetchAtlassian(credentials, path, {
184+
method: 'POST',
185+
body: requestBody,
186+
});
185187
return validateResponse(rawData, IssuesResponseSchema, 'issues search');
186188
} catch (error) {
187189
// McpError is already properly structured from fetchAtlassian or validation

src/services/vendor.atlassian.issues.test.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,21 @@ describe('Vendor Atlassian Issues Service', () => {
2424
it('should return a list of issues', async () => {
2525
if (skipIfNoCredentials()) return;
2626

27-
// Call the function with the real API
27+
// Call the function with the real API using bounded JQL query
2828
const result = await atlassianIssuesService.search({
2929
jql: 'created >= -30d',
3030
maxResults: 5,
3131
});
3232

33-
// Verify the response structure
33+
// Verify the response structure (new API response)
3434
expect(result).toHaveProperty('issues');
3535
expect(Array.isArray(result.issues)).toBe(true);
36-
expect(result).toHaveProperty('total');
37-
expect(result).toHaveProperty('maxResults');
38-
expect(result).toHaveProperty('startAt');
36+
// New API response structure
37+
expect(result).toHaveProperty('isLast');
38+
// nextPageToken is optional (only present if there are more pages)
39+
if (!result.isLast) {
40+
expect(result).toHaveProperty('nextPageToken');
41+
}
3942

4043
// If issues are returned, verify their structure
4144
if (result.issues.length > 0) {
@@ -48,29 +51,29 @@ describe('Vendor Atlassian Issues Service', () => {
4851
}
4952
}, 30000);
5053

51-
it('should handle pagination parameters', async () => {
54+
it('should handle pagination with nextPageToken', async () => {
5255
if (skipIfNoCredentials()) return;
5356

54-
// Call the function with pagination parameters
57+
// Call the function with pagination parameters and bounded JQL
5558
const result = await atlassianIssuesService.search({
59+
jql: 'created >= -30d',
5660
maxResults: 2,
57-
startAt: 0,
5861
});
5962

60-
// Verify pagination parameters are respected
61-
expect(result.maxResults).toBe(2);
62-
expect(result.startAt).toBe(0);
63+
// Verify response structure
64+
expect(result).toHaveProperty('issues');
6365
expect(result.issues.length).toBeLessThanOrEqual(2);
66+
expect(result).toHaveProperty('isLast');
6467

65-
// If there are more than 2 total issues, test the second page
66-
if (result.total > 2) {
68+
// If there are more pages, test pagination with nextPageToken
69+
if (!result.isLast && result.nextPageToken) {
6770
const page2 = await atlassianIssuesService.search({
71+
jql: 'created >= -30d',
6872
maxResults: 2,
69-
startAt: 2,
73+
nextPageToken: result.nextPageToken,
7074
});
7175

72-
expect(page2.startAt).toBe(2);
73-
expect(page2.maxResults).toBe(2);
76+
expect(page2).toHaveProperty('issues');
7477
expect(page2.issues.length).toBeLessThanOrEqual(2);
7578

7679
// If both pages have at least one issue, verify they're different
@@ -83,8 +86,9 @@ describe('Vendor Atlassian Issues Service', () => {
8386
it('should support searching with simple JQL (project=KEY)', async () => {
8487
if (skipIfNoCredentials()) return;
8588

86-
// First, get a list of issues to extract a project key
89+
// First, get a list of issues to extract a project key using bounded query
8790
const initialSearch = await atlassianIssuesService.search({
91+
jql: 'created >= -30d',
8892
maxResults: 1,
8993
});
9094

@@ -243,7 +247,8 @@ describe('Vendor Atlassian Issues Service', () => {
243247
expect(result).toHaveProperty('issues');
244248
expect(Array.isArray(result.issues)).toBe(true);
245249
expect(result.issues.length).toBe(0);
246-
expect(result.total).toBe(0);
250+
// New API doesn't return total, but should return isLast: true
251+
expect(result.isLast).toBe(true);
247252
}, 30000);
248253
});
249254

@@ -253,6 +258,7 @@ describe('Vendor Atlassian Issues Service', () => {
253258
if (skipIfNoCredentials()) return null;
254259
try {
255260
const searchResult = await atlassianIssuesService.search({
261+
jql: 'created >= -30d',
256262
maxResults: 1,
257263
});
258264

0 commit comments

Comments
 (0)