| import type { ConfigScope } from 'src/services/mcp/types.js' |
| import type { ZodError, ZodIssue } from 'zod/v4' |
| import { jsonParse } from '../slowOperations.js' |
| import { plural } from '../stringUtils.js' |
| import { validatePermissionRule } from './permissionValidation.js' |
| import { generateSettingsJSONSchema } from './schemaOutput.js' |
| import type { SettingsJson } from './types.js' |
| import { SettingsSchema } from './types.js' |
| import { getValidationTip } from './validationTips.js' |
|
|
| |
| |
| |
| |
| function isInvalidTypeIssue(issue: ZodIssue): issue is ZodIssue & { |
| code: 'invalid_type' |
| expected: string |
| input: unknown |
| } { |
| return issue.code === 'invalid_type' |
| } |
|
|
| function isInvalidValueIssue(issue: ZodIssue): issue is ZodIssue & { |
| code: 'invalid_value' |
| values: unknown[] |
| input: unknown |
| } { |
| return issue.code === 'invalid_value' |
| } |
|
|
| function isUnrecognizedKeysIssue( |
| issue: ZodIssue, |
| ): issue is ZodIssue & { code: 'unrecognized_keys'; keys: string[] } { |
| return issue.code === 'unrecognized_keys' |
| } |
|
|
| function isTooSmallIssue(issue: ZodIssue): issue is ZodIssue & { |
| code: 'too_small' |
| minimum: number | bigint |
| origin: string |
| } { |
| return issue.code === 'too_small' |
| } |
|
|
| |
| export type FieldPath = string |
|
|
| export type ValidationError = { |
| |
| file?: string |
| |
| path: FieldPath |
| |
| message: string |
| |
| expected?: string |
| |
| invalidValue?: unknown |
| |
| suggestion?: string |
| |
| docLink?: string |
| |
| mcpErrorMetadata?: { |
| |
| scope: ConfigScope |
| |
| serverName?: string |
| |
| severity?: 'fatal' | 'warning' |
| } |
| } |
|
|
| export type SettingsWithErrors = { |
| settings: SettingsJson |
| errors: ValidationError[] |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function getReceivedType(value: unknown): string { |
| if (value === null) return 'null' |
| if (value === undefined) return 'undefined' |
| if (Array.isArray(value)) return 'array' |
| return typeof value |
| } |
|
|
| function extractReceivedFromMessage(msg: string): string | undefined { |
| const match = msg.match(/received (\w+)/) |
| return match ? match[1] : undefined |
| } |
|
|
| export function formatZodError( |
| error: ZodError, |
| filePath: string, |
| ): ValidationError[] { |
| return error.issues.map((issue): ValidationError => { |
| const path = issue.path.map(String).join('.') |
| let message = issue.message |
| let expected: string | undefined |
|
|
| let enumValues: string[] | undefined |
| let expectedValue: string | undefined |
| let receivedValue: unknown |
| let invalidValue: unknown |
|
|
| if (isInvalidValueIssue(issue)) { |
| enumValues = issue.values.map(v => String(v)) |
| expectedValue = enumValues.join(' | ') |
| receivedValue = undefined |
| invalidValue = undefined |
| } else if (isInvalidTypeIssue(issue)) { |
| expectedValue = issue.expected |
| const receivedType = extractReceivedFromMessage(issue.message) |
| receivedValue = receivedType ?? getReceivedType(issue.input) |
| invalidValue = receivedType ?? getReceivedType(issue.input) |
| } else if (isTooSmallIssue(issue)) { |
| expectedValue = String(issue.minimum) |
| } else if (issue.code === 'custom' && 'params' in issue) { |
| const params = issue.params as { received?: unknown } |
| receivedValue = params.received |
| invalidValue = receivedValue |
| } |
|
|
| const tip = getValidationTip({ |
| path, |
| code: issue.code, |
| expected: expectedValue, |
| received: receivedValue, |
| enumValues, |
| message: issue.message, |
| value: receivedValue, |
| }) |
|
|
| if (isInvalidValueIssue(issue)) { |
| expected = enumValues?.map(v => `"${v}"`).join(', ') |
| message = `Invalid value. Expected one of: ${expected}` |
| } else if (isInvalidTypeIssue(issue)) { |
| const receivedType = |
| extractReceivedFromMessage(issue.message) ?? |
| getReceivedType(issue.input) |
| if ( |
| issue.expected === 'object' && |
| receivedType === 'null' && |
| path === '' |
| ) { |
| message = 'Invalid or malformed JSON' |
| } else { |
| message = `Expected ${issue.expected}, but received ${receivedType}` |
| } |
| } else if (isUnrecognizedKeysIssue(issue)) { |
| const keys = issue.keys.join(', ') |
| message = `Unrecognized ${plural(issue.keys.length, 'field')}: ${keys}` |
| } else if (isTooSmallIssue(issue)) { |
| message = `Number must be greater than or equal to ${issue.minimum}` |
| expected = String(issue.minimum) |
| } |
|
|
| return { |
| file: filePath, |
| path, |
| message, |
| expected, |
| invalidValue, |
| suggestion: tip?.suggestion, |
| docLink: tip?.docLink, |
| } |
| }) |
| } |
|
|
| |
| |
| |
| |
| export function validateSettingsFileContent(content: string): |
| | { |
| isValid: true |
| } |
| | { |
| isValid: false |
| error: string |
| fullSchema: string |
| } { |
| try { |
| |
| const jsonData = jsonParse(content) |
|
|
| |
| const result = SettingsSchema().strict().safeParse(jsonData) |
|
|
| if (result.success) { |
| return { isValid: true } |
| } |
|
|
| |
| const errors = formatZodError(result.error, 'settings') |
| const errorMessage = |
| 'Settings validation failed:\n' + |
| errors.map(err => `- ${err.path}: ${err.message}`).join('\n') |
|
|
| return { |
| isValid: false, |
| error: errorMessage, |
| fullSchema: generateSettingsJSONSchema(), |
| } |
| } catch (parseError) { |
| return { |
| isValid: false, |
| error: `Invalid JSON: ${parseError instanceof Error ? parseError.message : 'Unknown parsing error'}`, |
| fullSchema: generateSettingsJSONSchema(), |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function filterInvalidPermissionRules( |
| data: unknown, |
| filePath: string, |
| ): ValidationError[] { |
| if (!data || typeof data !== 'object') return [] |
| const obj = data as Record<string, unknown> |
| if (!obj.permissions || typeof obj.permissions !== 'object') return [] |
| const perms = obj.permissions as Record<string, unknown> |
|
|
| const warnings: ValidationError[] = [] |
| for (const key of ['allow', 'deny', 'ask']) { |
| const rules = perms[key] |
| if (!Array.isArray(rules)) continue |
|
|
| perms[key] = rules.filter(rule => { |
| if (typeof rule !== 'string') { |
| warnings.push({ |
| file: filePath, |
| path: `permissions.${key}`, |
| message: `Non-string value in ${key} array was removed`, |
| invalidValue: rule, |
| }) |
| return false |
| } |
| const result = validatePermissionRule(rule) |
| if (!result.valid) { |
| let message = `Invalid permission rule "${rule}" was skipped` |
| if (result.error) message += `: ${result.error}` |
| if (result.suggestion) message += `. ${result.suggestion}` |
| warnings.push({ |
| file: filePath, |
| path: `permissions.${key}`, |
| message, |
| invalidValue: rule, |
| }) |
| return false |
| } |
| return true |
| }) |
| } |
| return warnings |
| } |
|
|