Skip to content

Commit 9d731b6

Browse files
fix: Zodios Validation Errors in MCP Tools (#465)
* fix: Zodios Validation Errors in MCP Tools * chore: cleanup TS comments
1 parent eafe7d1 commit 9d731b6

File tree

10 files changed

+368
-125
lines changed

10 files changed

+368
-125
lines changed

src/api/apiClient.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,18 @@ export const errorMap = (issue: ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
109109
}
110110
}
111111

112-
export const apiClient = createApiClient(BASE_URL, {
112+
// TLDR: the inferred TS schema was too big, so this is a workaround to fix it.
113+
// Create intermediate type alias to break complex type inference
114+
const _createApiClient = createApiClient
115+
type ApiClientType = ReturnType<typeof _createApiClient>
116+
117+
// Create the actual instance with explicit type annotation
118+
const apiClient: ApiClientType = _createApiClient(BASE_URL, {
113119
axiosInstance: axiosClient,
114120
validate: 'request',
115-
})
121+
}) as ApiClientType
122+
123+
export { apiClient }
116124
export default apiClient
117125

118126
export const v2ApiClient = createV2ApiClient(BASE_URL)

src/api/zodClient.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,29 +202,59 @@ const UpdateEnvironmentDto = z
202202
const GenerateSdkTokensDto = z
203203
.object({ client: z.boolean(), server: z.boolean(), mobile: z.boolean() })
204204
.partial()
205-
const AllFilter = z.object({ type: z.literal('all').default('all') })
206-
const OptInFilter = z.object({ type: z.literal('optIn').default('optIn') })
205+
const AllFilter = z.object({
206+
type: z.literal('all').default('all'),
207+
_audiences: z.array(z.string()).optional(),
208+
values: z.array(z.string()).optional(),
209+
})
210+
const OptInFilter = z.object({
211+
type: z.literal('optIn').default('optIn'),
212+
_audiences: z.array(z.string()).optional(),
213+
values: z.array(z.string()).optional(),
214+
})
207215
const UserFilter = z.object({
208216
subType: z.enum(['user_id', 'email', 'platform', 'deviceModel']),
209-
comparator: z.enum(['=', '!=', 'exist', '!exist', 'contain', '!contain']),
217+
comparator: z.enum([
218+
'=',
219+
'!=',
220+
'exist',
221+
'!exist',
222+
'contain',
223+
'!contain',
224+
'endWith',
225+
'startWith',
226+
]),
210227
values: z.array(z.string()).optional(),
228+
_audiences: z.array(z.string()).optional(),
211229
type: z.literal('user').default('user'),
212230
})
213231
const UserCountryFilter = z.object({
214232
subType: z.literal('country').default('country'),
215-
comparator: z.enum(['=', '!=', 'exist', '!exist', 'contain', '!contain']),
233+
comparator: z.enum([
234+
'=',
235+
'!=',
236+
'exist',
237+
'!exist',
238+
'contain',
239+
'!contain',
240+
'endWith',
241+
'startWith',
242+
]),
216243
values: z.array(z.string()),
244+
_audiences: z.array(z.string()).optional(),
217245
type: z.literal('user').default('user'),
218246
})
219247
const UserAppVersionFilter = z.object({
220248
comparator: z.enum(['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist']),
221249
values: z.array(z.string()).optional(),
250+
_audiences: z.array(z.string()).optional(),
222251
type: z.literal('user').default('user'),
223252
subType: z.literal('appVersion').default('appVersion'),
224253
})
225254
const UserPlatformVersionFilter = z.object({
226255
comparator: z.enum(['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist']),
227256
values: z.array(z.string()).optional(),
257+
_audiences: z.array(z.string()).optional(),
228258
type: z.literal('user').default('user'),
229259
subType: z.literal('platformVersion').default('platformVersion'),
230260
})
@@ -244,6 +274,7 @@ const UserCustomFilter = z.object({
244274
dataKey: z.string().min(1),
245275
dataKeyType: z.enum(['String', 'Boolean', 'Number']),
246276
values: z.array(z.union([z.boolean(), z.string(), z.number()])).optional(),
277+
_audiences: z.array(z.string()).optional(),
247278
type: z.literal('user').default('user'),
248279
subType: z.literal('customData').default('customData'),
249280
})
@@ -564,8 +595,10 @@ const Target = z.object({
564595
_id: z.string(),
565596
name: z.string().optional(),
566597
audience: TargetAudience,
598+
filters: z.array(z.any()).optional(),
567599
rollout: Rollout.nullable().optional(),
568600
distribution: z.array(TargetDistribution),
601+
bucketingKey: z.string().optional(),
569602
})
570603
const FeatureConfig = z.object({
571604
_feature: z.string(),
@@ -576,6 +609,7 @@ const FeatureConfig = z.object({
576609
updatedAt: z.string().datetime(),
577610
targets: z.array(Target),
578611
readonly: z.boolean(),
612+
hasStaticConfig: z.boolean().optional(),
579613
})
580614
const UpdateTargetDto = z.object({
581615
_id: z.string().optional(),

src/mcp/server.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,41 @@ import {
2828
selfTargetingToolHandlers,
2929
} from './tools/selfTargetingTools'
3030

31+
// Environment variable to control output schema inclusion
32+
const ENABLE_OUTPUT_SCHEMAS = process.env.ENABLE_OUTPUT_SCHEMAS === 'true'
33+
if (ENABLE_OUTPUT_SCHEMAS) {
34+
console.error('DevCycle MCP Server - Output Schemas: ENABLED')
35+
}
36+
37+
const ENABLE_DVC_MCP_DEBUG = process.env.ENABLE_DVC_MCP_DEBUG === 'true'
38+
3139
// Tool handler function type
3240
export type ToolHandler = (
3341
args: unknown,
3442
apiClient: DevCycleApiClient,
3543
) => Promise<any>
3644

45+
// Function to conditionally remove outputSchema from tool definitions
46+
const processToolDefinitions = (tools: Tool[]): Tool[] => {
47+
if (ENABLE_OUTPUT_SCHEMAS) {
48+
return tools
49+
}
50+
51+
// Remove outputSchema from all tools when disabled
52+
return tools.map((tool) => {
53+
const { outputSchema, ...toolWithoutSchema } = tool
54+
return toolWithoutSchema
55+
})
56+
}
57+
3758
// Combine all tool definitions
38-
const allToolDefinitions: Tool[] = [
59+
const allToolDefinitions: Tool[] = processToolDefinitions([
3960
...featureToolDefinitions,
4061
...environmentToolDefinitions,
4162
...projectToolDefinitions,
4263
...variableToolDefinitions,
4364
...selfTargetingToolDefinitions,
44-
]
65+
])
4566

4667
// Combine all tool handlers
4768
const allToolHandlers: Record<string, ToolHandler> = {
@@ -241,14 +262,50 @@ export class DevCycleMCPServer {
241262
}
242263

243264
const result = await handler(args, this.apiClient)
244-
return {
265+
266+
// Return structured content only if output schemas are enabled
267+
if (ENABLE_OUTPUT_SCHEMAS) {
268+
// Check if tool has output schema
269+
const toolDef = allToolDefinitions.find(
270+
(tool) => tool.name === name,
271+
)
272+
273+
if (toolDef?.outputSchema) {
274+
// For tools with output schemas, return structured JSON content
275+
const mcpResult = {
276+
content: [
277+
{
278+
type: 'json',
279+
json: result,
280+
},
281+
],
282+
}
283+
if (ENABLE_DVC_MCP_DEBUG) {
284+
console.error(
285+
`MCP ${name} structured JSON result:`,
286+
JSON.stringify(mcpResult, null, 2),
287+
)
288+
}
289+
return mcpResult
290+
}
291+
}
292+
293+
// Default: return as text content (for disabled schemas or tools without schemas)
294+
const mcpResult = {
245295
content: [
246296
{
247297
type: 'text',
248298
text: JSON.stringify(result, null, 2),
249299
},
250300
],
251301
}
302+
if (ENABLE_DVC_MCP_DEBUG) {
303+
console.error(
304+
`MCP ${name} text result:`,
305+
JSON.stringify(mcpResult, null, 2),
306+
)
307+
}
308+
return mcpResult
252309
} catch (error) {
253310
return this.handleToolError(error, name)
254311
}

src/mcp/tools/environmentTools.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Tool } from '@modelcontextprotocol/sdk/types.js'
2-
import { DevCycleApiClient } from '../utils/api'
2+
import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api'
33
import {
44
fetchEnvironments,
55
fetchEnvironmentByKey,
@@ -264,7 +264,10 @@ export const environmentToolHandlers: Record<string, ToolHandler> = {
264264
'listEnvironments',
265265
validatedArgs,
266266
async (authToken, projectKey) => {
267-
return await fetchEnvironments(authToken, projectKey)
267+
return await handleZodiosValidationErrors(
268+
() => fetchEnvironments(authToken, projectKey),
269+
'listEnvironments',
270+
)
268271
},
269272
generateEnvironmentDashboardLink,
270273
)
@@ -276,10 +279,14 @@ export const environmentToolHandlers: Record<string, ToolHandler> = {
276279
'getSdkKeys',
277280
validatedArgs,
278281
async (authToken, projectKey) => {
279-
const environment = await fetchEnvironmentByKey(
280-
authToken,
281-
projectKey,
282-
validatedArgs.environmentKey,
282+
const environment = await handleZodiosValidationErrors(
283+
() =>
284+
fetchEnvironmentByKey(
285+
authToken,
286+
projectKey,
287+
validatedArgs.environmentKey,
288+
),
289+
'fetchEnvironmentByKey',
283290
)
284291

285292
const sdkKeys = environment.sdkKeys
@@ -306,10 +313,10 @@ export const environmentToolHandlers: Record<string, ToolHandler> = {
306313
'createEnvironment',
307314
validatedArgs,
308315
async (authToken, projectKey) => {
309-
return await createEnvironment(
310-
authToken,
311-
projectKey,
312-
validatedArgs,
316+
return await handleZodiosValidationErrors(
317+
() =>
318+
createEnvironment(authToken, projectKey, validatedArgs),
319+
'createEnvironment',
313320
)
314321
},
315322
generateEnvironmentDashboardLink,
@@ -323,11 +330,15 @@ export const environmentToolHandlers: Record<string, ToolHandler> = {
323330
validatedArgs,
324331
async (authToken, projectKey) => {
325332
const { key, ...updateParams } = validatedArgs
326-
return await updateEnvironment(
327-
authToken,
328-
projectKey,
329-
key,
330-
updateParams,
333+
return await handleZodiosValidationErrors(
334+
() =>
335+
updateEnvironment(
336+
authToken,
337+
projectKey,
338+
key,
339+
updateParams,
340+
),
341+
'updateEnvironment',
331342
)
332343
},
333344
generateEnvironmentDashboardLink,

0 commit comments

Comments
 (0)