import { z } from 'zod'; /** * Shared validation schemas for Conversation MCP API responses * * CRITICAL: These validators enforce the contract that the frontend depends on. * They should FAIL when responses contain serialized JSON strings instead of proper objects. * * Usage: * - Backend: Validate responses before returning to MCP clients * - Frontend: Validate responses after parsing from API * - Tests: Assert on response structure */ // Base message role enum export const ConversationMessageRoleSchema = z.enum(['user', 'assistant', 'system', 'tool', 'function']); const ModelOpinionSummarySchema = z.object({ model: z.string().min(1, 'Model name must not be empty'), response: z.string(), tokensUsed: z.number().int().nonnegative().optional(), latencyMs: z.number().nonnegative().optional(), citations: z.array(z.object({ title: z.string(), url: z.string().url(), snippet: z.string().optional() }).passthrough()).optional() }).passthrough(); const SynthesisSummarySchema = z.object({ model: z.string().min(1, 'Synthesis must include model name'), response: z.string().min(1, 'Synthesis must include response text'), modelId: z.string().optional(), modelDisplayName: z.string().optional(), tokensUsed: z.number().int().nonnegative().optional(), latencyMs: z.number().nonnegative().optional(), cost: z.number().optional(), sources: z.array(z.any()).optional(), error: z.boolean().optional() }).passthrough(); const PersistenceStatusSchema = z.object({ userMessageSaved: z.boolean().optional(), assistantMessageSaved: z.boolean().optional(), userMessageId: z.string().nullable().optional(), assistantMessageId: z.string().nullable().optional(), error: z.string().nullable().optional() }).passthrough(); const MessageMetadataSchema = z.object({ secondOpinion: z.object({ primary: ModelOpinionSummarySchema, secondaryOpinions: z.array(ModelOpinionSummarySchema).optional(), synthesis: SynthesisSummarySchema.optional() }).optional(), persistenceStatus: PersistenceStatusSchema.optional() }).passthrough(); // Core message schema - messages should be objects, NOT serialized JSON strings export const ConversationMessageSchema = z.object({ id: z.string().min(1, 'Message ID must not be empty'), conversationId: z.string().optional(), role: ConversationMessageRoleSchema, content: z.string(), userId: z.string().optional(), sequence: z.number().int().nonnegative().optional(), createdAt: z.string().datetime({ message: 'createdAt must be ISO 8601 datetime' }), metadata: MessageMetadataSchema.optional() }).passthrough(); // Allow additional fields for extensibility // History response schema export const ConversationHistoryResponseSchema = z.object({ history: z.object({ conversationId: z.string().optional(), messages: z.array(ConversationMessageSchema).optional(), messageCount: z.number().int().nonnegative().optional(), hasMore: z.boolean().optional(), nextCursor: z.string().nullable().optional(), }).passthrough().optional(), }).passthrough(); // Send message response schema export const SendMessageResponseSchema = z.object({ conversationId: z.string().min(1, 'conversationId must not be empty').optional(), messageId: z.string().min(1, 'messageId must not be empty').optional(), sequence: z.number().int().nonnegative().optional(), created: z.boolean().optional(), title: z.string().nullable().optional(), timestamp: z.string().datetime().optional(), message: ConversationMessageSchema.optional(), assistantMessage: ConversationMessageSchema.optional(), metadata: MessageMetadataSchema.optional(), }).passthrough(); // List conversations response schema export const ListConversationsResponseSchema = z.object({ conversations: z.array(z.object({ id: z.string().min(1), // Fixed: Backend returns 'id', not 'conversationId' title: z.string().nullable().optional(), createdAt: z.string().datetime().optional(), lastMessageAt: z.string().datetime().optional(), messageCount: z.number().int().nonnegative().optional(), }).passthrough()).optional(), hasMore: z.boolean().optional(), nextCursor: z.string().nullable().optional(), }).passthrough(); /** * Validate conversation.get-history response * * CRITICAL: This will FAIL if the response contains serialized JSON strings * instead of proper structured objects. This is intentional - it should drive * fixing the backend to return proper objects. */ export function validateHistoryResponse(response: unknown): z.infer { try { return ConversationHistoryResponseSchema.parse(response); } catch (error) { if (error instanceof z.ZodError) { throw new Error( `conversation.get-history response validation failed: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}` ); } throw error; } } /** * Validate conversation.send-message response */ export function validateSendMessageResponse(response: unknown): z.infer { try { return SendMessageResponseSchema.parse(response); } catch (error) { if (error instanceof z.ZodError) { throw new Error( `conversation.send-message response validation failed: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}` ); } throw error; } } /** * Validate conversation.list response */ export function validateListConversationsResponse(response: unknown): z.infer { try { return ListConversationsResponseSchema.parse(response); } catch (error) { if (error instanceof z.ZodError) { throw new Error( `conversation.list response validation failed: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}` ); } throw error; } } /** * Type exports for TypeScript consumers */ export type ConversationMessage = z.infer; export type ConversationHistoryResponse = z.infer; export type SendMessageResponse = z.infer; export type ListConversationsResponse = z.infer; export type ConversationMessageMetadata = z.infer;