Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,18 @@ export const errorMap = (issue: ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
}
}

export const apiClient = createApiClient(BASE_URL, {
// TLDR: the inferred TS schema was too big, so this is a workaround to fix it.
// Create intermediate type alias to break complex type inference
const _createApiClient = createApiClient
type ApiClientType = ReturnType<typeof _createApiClient>

// Create the actual instance with explicit type annotation
const apiClient: ApiClientType = _createApiClient(BASE_URL, {
axiosInstance: axiosClient,
validate: 'request',
})
}) as ApiClientType

export { apiClient }
export default apiClient

export const v2ApiClient = createV2ApiClient(BASE_URL)
42 changes: 38 additions & 4 deletions src/api/zodClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,29 +202,59 @@ const UpdateEnvironmentDto = z
const GenerateSdkTokensDto = z
.object({ client: z.boolean(), server: z.boolean(), mobile: z.boolean() })
.partial()
const AllFilter = z.object({ type: z.literal('all').default('all') })
const OptInFilter = z.object({ type: z.literal('optIn').default('optIn') })
const AllFilter = z.object({
type: z.literal('all').default('all'),
_audiences: z.array(z.string()).optional(),
values: z.array(z.string()).optional(),
})
const OptInFilter = z.object({
type: z.literal('optIn').default('optIn'),
_audiences: z.array(z.string()).optional(),
values: z.array(z.string()).optional(),
})
const UserFilter = z.object({
subType: z.enum(['user_id', 'email', 'platform', 'deviceModel']),
comparator: z.enum(['=', '!=', 'exist', '!exist', 'contain', '!contain']),
comparator: z.enum([
'=',
'!=',
'exist',
'!exist',
'contain',
'!contain',
'endWith',
'startWith',
]),
values: z.array(z.string()).optional(),
_audiences: z.array(z.string()).optional(),
type: z.literal('user').default('user'),
})
const UserCountryFilter = z.object({
subType: z.literal('country').default('country'),
comparator: z.enum(['=', '!=', 'exist', '!exist', 'contain', '!contain']),
comparator: z.enum([
'=',
'!=',
'exist',
'!exist',
'contain',
'!contain',
'endWith',
'startWith',
]),
values: z.array(z.string()),
_audiences: z.array(z.string()).optional(),
type: z.literal('user').default('user'),
})
const UserAppVersionFilter = z.object({
comparator: z.enum(['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist']),
values: z.array(z.string()).optional(),
_audiences: z.array(z.string()).optional(),
type: z.literal('user').default('user'),
subType: z.literal('appVersion').default('appVersion'),
})
const UserPlatformVersionFilter = z.object({
comparator: z.enum(['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist']),
values: z.array(z.string()).optional(),
_audiences: z.array(z.string()).optional(),
type: z.literal('user').default('user'),
subType: z.literal('platformVersion').default('platformVersion'),
})
Expand All @@ -244,6 +274,7 @@ const UserCustomFilter = z.object({
dataKey: z.string().min(1),
dataKeyType: z.enum(['String', 'Boolean', 'Number']),
values: z.array(z.union([z.boolean(), z.string(), z.number()])).optional(),
_audiences: z.array(z.string()).optional(),
type: z.literal('user').default('user'),
subType: z.literal('customData').default('customData'),
})
Expand Down Expand Up @@ -564,8 +595,10 @@ const Target = z.object({
_id: z.string(),
name: z.string().optional(),
audience: TargetAudience,
filters: z.array(z.any()).optional(),
rollout: Rollout.nullable().optional(),
distribution: z.array(TargetDistribution),
bucketingKey: z.string().optional(),
})
const FeatureConfig = z.object({
_feature: z.string(),
Expand All @@ -576,6 +609,7 @@ const FeatureConfig = z.object({
updatedAt: z.string().datetime(),
targets: z.array(Target),
readonly: z.boolean(),
hasStaticConfig: z.boolean().optional(),
})
const UpdateTargetDto = z.object({
_id: z.string().optional(),
Expand Down
63 changes: 60 additions & 3 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,41 @@ import {
selfTargetingToolHandlers,
} from './tools/selfTargetingTools'

// Environment variable to control output schema inclusion
const ENABLE_OUTPUT_SCHEMAS = process.env.ENABLE_OUTPUT_SCHEMAS === 'true'
if (ENABLE_OUTPUT_SCHEMAS) {
console.error('DevCycle MCP Server - Output Schemas: ENABLED')
}

const ENABLE_DVC_MCP_DEBUG = process.env.ENABLE_DVC_MCP_DEBUG === 'true'

// Tool handler function type
export type ToolHandler = (
args: unknown,
apiClient: DevCycleApiClient,
) => Promise<any>

// Function to conditionally remove outputSchema from tool definitions
const processToolDefinitions = (tools: Tool[]): Tool[] => {
if (ENABLE_OUTPUT_SCHEMAS) {
return tools
}

// Remove outputSchema from all tools when disabled
return tools.map((tool) => {
const { outputSchema, ...toolWithoutSchema } = tool
return toolWithoutSchema
})
}

// Combine all tool definitions
const allToolDefinitions: Tool[] = [
const allToolDefinitions: Tool[] = processToolDefinitions([
...featureToolDefinitions,
...environmentToolDefinitions,
...projectToolDefinitions,
...variableToolDefinitions,
...selfTargetingToolDefinitions,
]
])

// Combine all tool handlers
const allToolHandlers: Record<string, ToolHandler> = {
Expand Down Expand Up @@ -241,14 +262,50 @@ export class DevCycleMCPServer {
}

const result = await handler(args, this.apiClient)
return {

// Return structured content only if output schemas are enabled
if (ENABLE_OUTPUT_SCHEMAS) {
// Check if tool has output schema
const toolDef = allToolDefinitions.find(
(tool) => tool.name === name,
)

if (toolDef?.outputSchema) {
// For tools with output schemas, return structured JSON content
const mcpResult = {
content: [
{
type: 'json',
json: result,
},
],
}
if (ENABLE_DVC_MCP_DEBUG) {
console.error(
`MCP ${name} structured JSON result:`,
JSON.stringify(mcpResult, null, 2),
)
}
return mcpResult
}
}

// Default: return as text content (for disabled schemas or tools without schemas)
const mcpResult = {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
}
if (ENABLE_DVC_MCP_DEBUG) {
console.error(
`MCP ${name} text result:`,
JSON.stringify(mcpResult, null, 2),
)
}
return mcpResult
} catch (error) {
return this.handleToolError(error, name)
}
Expand Down
41 changes: 26 additions & 15 deletions src/mcp/tools/environmentTools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { DevCycleApiClient } from '../utils/api'
import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api'
import {
fetchEnvironments,
fetchEnvironmentByKey,
Expand Down Expand Up @@ -264,7 +264,10 @@ export const environmentToolHandlers: Record<string, ToolHandler> = {
'listEnvironments',
validatedArgs,
async (authToken, projectKey) => {
return await fetchEnvironments(authToken, projectKey)
return await handleZodiosValidationErrors(
() => fetchEnvironments(authToken, projectKey),
'listEnvironments',
)
},
generateEnvironmentDashboardLink,
)
Expand All @@ -276,10 +279,14 @@ export const environmentToolHandlers: Record<string, ToolHandler> = {
'getSdkKeys',
validatedArgs,
async (authToken, projectKey) => {
const environment = await fetchEnvironmentByKey(
authToken,
projectKey,
validatedArgs.environmentKey,
const environment = await handleZodiosValidationErrors(
() =>
fetchEnvironmentByKey(
authToken,
projectKey,
validatedArgs.environmentKey,
),
'fetchEnvironmentByKey',
)

const sdkKeys = environment.sdkKeys
Expand All @@ -306,10 +313,10 @@ export const environmentToolHandlers: Record<string, ToolHandler> = {
'createEnvironment',
validatedArgs,
async (authToken, projectKey) => {
return await createEnvironment(
authToken,
projectKey,
validatedArgs,
return await handleZodiosValidationErrors(
() =>
createEnvironment(authToken, projectKey, validatedArgs),
'createEnvironment',
)
},
generateEnvironmentDashboardLink,
Expand All @@ -323,11 +330,15 @@ export const environmentToolHandlers: Record<string, ToolHandler> = {
validatedArgs,
async (authToken, projectKey) => {
const { key, ...updateParams } = validatedArgs
return await updateEnvironment(
authToken,
projectKey,
key,
updateParams,
return await handleZodiosValidationErrors(
() =>
updateEnvironment(
authToken,
projectKey,
key,
updateParams,
),
'updateEnvironment',
)
},
generateEnvironmentDashboardLink,
Expand Down
Loading