Skip to content

Commit 00085d2

Browse files
committed
fix: resolve MCP array serialization schema issue
- Remove transform functions from array schemas in common.ts and issues.ts - Add JSON string parsing logic to handlers before schema validation - Preserve backward compatibility by accepting both arrays and JSON strings This fixes the issue where MCP clients receive incomplete schemas that don't include the string option for array parameters, causing validation errors when clients send JSON-serialized arrays.
1 parent 309cd31 commit 00085d2

File tree

3 files changed

+26
-89
lines changed

3 files changed

+26
-89
lines changed

src/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,22 @@ export {
146146
handleSonarQubeUpdateHotspotStatus,
147147
} from './handlers/index.js';
148148

149+
// Helper function to parse JSON string arrays in parameters
150+
function parseArrayParameters(params: Record<string, unknown>, paramNames: string[]): void {
151+
for (const key of paramNames) {
152+
if (typeof params[key] === 'string') {
153+
try {
154+
const parsed = JSON.parse(params[key] as string);
155+
if (Array.isArray(parsed)) {
156+
params[key] = parsed;
157+
}
158+
} catch {
159+
// Not valid JSON, keep as string for backward compatibility
160+
}
161+
}
162+
}
163+
}
164+
149165
// Lambda functions for the MCP tools
150166
/**
151167
* Lambda function for projects tool
@@ -166,6 +182,16 @@ export const metricsHandler = async (params: { page: number | null; page_size: n
166182
* Lambda function for issues tool
167183
*/
168184
export const issuesHandler = async (params: Record<string, unknown>) => {
185+
// Parse JSON strings to arrays for array parameters
186+
parseArrayParameters(params, [
187+
'projects', 'component_keys', 'components', 'directories', 'files',
188+
'scopes', 'issues', 'severities', 'statuses', 'resolutions', 'types',
189+
'clean_code_attribute_categories', 'impact_severities', 'impact_software_qualities',
190+
'issue_statuses', 'rules', 'tags', 'assignees', 'authors', 'cwe',
191+
'owasp_top10', 'owasp_top10_v2021', 'sans_top25', 'sonarsource_security',
192+
'sonarsource_security_category', 'languages', 'facets', 'additional_fields'
193+
]);
194+
169195
return handleSonarQubeGetIssues(mapToSonarQubeParams(params));
170196
};
171197

src/schemas/common.ts

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,102 +13,40 @@ export const severitySchema = z
1313

1414
export const severitiesSchema = z
1515
.union([z.array(z.enum(['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'])), z.string()])
16-
.transform((val) => {
17-
const parsed = parseJsonStringArray(val);
18-
// Validate that all values are valid severities
19-
if (parsed && Array.isArray(parsed)) {
20-
return parsed.filter((v) => ['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'].includes(v));
21-
}
22-
return parsed;
23-
})
2416
.nullable()
2517
.optional();
2618

2719
// Status schemas
2820
export const statusSchema = z
2921
.union([z.array(z.enum(['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED'])), z.string()])
30-
.transform((val) => {
31-
const parsed = parseJsonStringArray(val);
32-
// Validate that all values are valid statuses
33-
if (parsed && Array.isArray(parsed)) {
34-
return parsed.filter((v) =>
35-
['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED'].includes(v)
36-
);
37-
}
38-
return parsed;
39-
})
4022
.nullable()
4123
.optional();
4224

4325
// Resolution schemas
4426
export const resolutionSchema = z
4527
.union([z.array(z.enum(['FALSE-POSITIVE', 'WONTFIX', 'FIXED', 'REMOVED'])), z.string()])
46-
.transform((val) => {
47-
const parsed = parseJsonStringArray(val);
48-
// Validate that all values are valid resolutions
49-
if (parsed && Array.isArray(parsed)) {
50-
return parsed.filter((v) => ['FALSE-POSITIVE', 'WONTFIX', 'FIXED', 'REMOVED'].includes(v));
51-
}
52-
return parsed;
53-
})
5428
.nullable()
5529
.optional();
5630

5731
// Type schemas
5832
export const typeSchema = z
5933
.union([z.array(z.enum(['CODE_SMELL', 'BUG', 'VULNERABILITY', 'SECURITY_HOTSPOT'])), z.string()])
60-
.transform((val) => {
61-
const parsed = parseJsonStringArray(val);
62-
// Validate that all values are valid types
63-
if (parsed && Array.isArray(parsed)) {
64-
return parsed.filter((v) =>
65-
['CODE_SMELL', 'BUG', 'VULNERABILITY', 'SECURITY_HOTSPOT'].includes(v)
66-
);
67-
}
68-
return parsed;
69-
})
7034
.nullable()
7135
.optional();
7236

7337
// Clean Code taxonomy schemas
7438
export const cleanCodeAttributeCategoriesSchema = z
7539
.union([z.array(z.enum(['ADAPTABLE', 'CONSISTENT', 'INTENTIONAL', 'RESPONSIBLE'])), z.string()])
76-
.transform((val) => {
77-
const parsed = parseJsonStringArray(val);
78-
// Validate that all values are valid categories
79-
if (parsed && Array.isArray(parsed)) {
80-
return parsed.filter((v) =>
81-
['ADAPTABLE', 'CONSISTENT', 'INTENTIONAL', 'RESPONSIBLE'].includes(v)
82-
);
83-
}
84-
return parsed;
85-
})
8640
.nullable()
8741
.optional();
8842

8943
export const impactSeveritiesSchema = z
9044
.union([z.array(z.enum(['HIGH', 'MEDIUM', 'LOW'])), z.string()])
91-
.transform((val) => {
92-
const parsed = parseJsonStringArray(val);
93-
// Validate that all values are valid impact severities
94-
if (parsed && Array.isArray(parsed)) {
95-
return parsed.filter((v) => ['HIGH', 'MEDIUM', 'LOW'].includes(v));
96-
}
97-
return parsed;
98-
})
9945
.nullable()
10046
.optional();
10147

10248
export const impactSoftwareQualitiesSchema = z
10349
.union([z.array(z.enum(['MAINTAINABILITY', 'RELIABILITY', 'SECURITY'])), z.string()])
104-
.transform((val) => {
105-
const parsed = parseJsonStringArray(val);
106-
// Validate that all values are valid software qualities
107-
if (parsed && Array.isArray(parsed)) {
108-
return parsed.filter((v) => ['MAINTAINABILITY', 'RELIABILITY', 'SECURITY'].includes(v));
109-
}
110-
return parsed;
111-
})
11250
.nullable()
11351
.optional();
11452

src/schemas/issues.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -130,21 +130,18 @@ export const issuesToolSchema = {
130130
project_key: z.string().optional().describe('Single project key for backward compatibility'), // Made optional to support projects array
131131
projects: z
132132
.union([z.array(z.string()), z.string()])
133-
.transform(parseJsonStringArray)
134133
.nullable()
135134
.optional()
136135
.describe('Filter by project keys'),
137136
component_keys: z
138137
.union([z.array(z.string()), z.string()])
139-
.transform(parseJsonStringArray)
140138
.nullable()
141139
.optional()
142140
.describe(
143141
'Filter by component keys (file paths, directories, or modules). Use this to filter issues by specific files or folders'
144142
),
145143
components: z
146144
.union([z.array(z.string()), z.string()])
147-
.transform(parseJsonStringArray)
148145
.nullable()
149146
.optional()
150147
.describe('Alias for component_keys - filter by file paths, directories, or modules'),
@@ -155,26 +152,16 @@ export const issuesToolSchema = {
155152
.describe('Return only issues on the specified components, not on their sub-components'),
156153
directories: z
157154
.union([z.array(z.string()), z.string()])
158-
.transform(parseJsonStringArray)
159155
.nullable()
160156
.optional()
161157
.describe('Filter by directory paths'),
162158
files: z
163159
.union([z.array(z.string()), z.string()])
164-
.transform(parseJsonStringArray)
165160
.nullable()
166161
.optional()
167162
.describe('Filter by specific file paths'),
168163
scopes: z
169164
.union([z.array(z.enum(['MAIN', 'TEST', 'OVERALL'])), z.string()])
170-
.transform((val) => {
171-
const parsed = parseJsonStringArray(val);
172-
// Validate that all values are valid scopes
173-
if (parsed && Array.isArray(parsed)) {
174-
return parsed.filter((v) => ['MAIN', 'TEST', 'OVERALL'].includes(v));
175-
}
176-
return parsed;
177-
})
178165
.nullable()
179166
.optional()
180167
.describe('Filter by issue scopes (MAIN, TEST, OVERALL)'),
@@ -186,7 +173,6 @@ export const issuesToolSchema = {
186173
// Issue filters
187174
issues: z
188175
.union([z.array(z.string()), z.string()])
189-
.transform(parseJsonStringArray)
190176
.nullable()
191177
.optional(),
192178
severity: severitySchema, // Deprecated single value
@@ -208,13 +194,11 @@ export const issuesToolSchema = {
208194
// Rules and tags
209195
rules: z
210196
.union([z.array(z.string()), z.string()])
211-
.transform(parseJsonStringArray)
212197
.nullable()
213198
.optional()
214199
.describe('Filter by rule keys'),
215200
tags: z
216201
.union([z.array(z.string()), z.string()])
217-
.transform(parseJsonStringArray)
218202
.nullable()
219203
.optional()
220204
.describe(
@@ -235,7 +219,6 @@ export const issuesToolSchema = {
235219
.describe('Filter to only assigned (true) or unassigned (false) issues'),
236220
assignees: z
237221
.union([z.array(z.string()), z.string()])
238-
.transform(parseJsonStringArray)
239222
.nullable()
240223
.optional()
241224
.describe(
@@ -244,54 +227,45 @@ export const issuesToolSchema = {
244227
author: z.string().nullable().optional().describe('Filter by single issue author'), // Single author
245228
authors: z
246229
.union([z.array(z.string()), z.string()])
247-
.transform(parseJsonStringArray)
248230
.nullable()
249231
.optional()
250232
.describe('Filter by multiple issue authors'), // Multiple authors
251233

252234
// Security standards
253235
cwe: z
254236
.union([z.array(z.string()), z.string()])
255-
.transform(parseJsonStringArray)
256237
.nullable()
257238
.optional(),
258239
owasp_top10: z
259240
.union([z.array(z.string()), z.string()])
260-
.transform(parseJsonStringArray)
261241
.nullable()
262242
.optional(),
263243
owasp_top10_v2021: z
264244
.union([z.array(z.string()), z.string()])
265-
.transform(parseJsonStringArray)
266245
.nullable()
267246
.optional(), // New 2021 version
268247
sans_top25: z
269248
.union([z.array(z.string()), z.string()])
270-
.transform(parseJsonStringArray)
271249
.nullable()
272250
.optional(),
273251
sonarsource_security: z
274252
.union([z.array(z.string()), z.string()])
275-
.transform(parseJsonStringArray)
276253
.nullable()
277254
.optional(),
278255
sonarsource_security_category: z
279256
.union([z.array(z.string()), z.string()])
280-
.transform(parseJsonStringArray)
281257
.nullable()
282258
.optional(),
283259

284260
// Languages
285261
languages: z
286262
.union([z.array(z.string()), z.string()])
287-
.transform(parseJsonStringArray)
288263
.nullable()
289264
.optional(),
290265

291266
// Facets
292267
facets: z
293268
.union([z.array(z.string()), z.string()])
294-
.transform(parseJsonStringArray)
295269
.nullable()
296270
.optional()
297271
.describe(
@@ -325,7 +299,6 @@ export const issuesToolSchema = {
325299
// Response optimization
326300
additional_fields: z
327301
.union([z.array(z.string()), z.string()])
328-
.transform(parseJsonStringArray)
329302
.nullable()
330303
.optional(),
331304

0 commit comments

Comments
 (0)