Skip to main content
Glama

Context Pods

by conorluddy
output-validator.tsโ€ข8.12 kB
/** * Output validation for wrapped scripts */ import { z, type ZodSchema } from 'zod'; /** * Output validator for wrapped scripts */ export class OutputValidator { /** * Validate script output against expected value */ validateOutput(actual: unknown, expected: unknown): void { // If expected is a schema, validate against it if (this.isZodSchema(expected)) { const result = expected.safeParse(actual); if (!result.success) { throw new Error(`Output validation failed: ${result.error.message}`); } return; } // If expected is a function, use it as validator if (typeof expected === 'function') { const isValid = (expected as (val: unknown) => boolean)(actual); if (!isValid) { throw new Error('Output validation failed: custom validator returned false'); } return; } // Otherwise, do deep equality check if (!this.deepEqual(actual, expected)) { throw new Error( `Output mismatch:\nExpected: ${JSON.stringify(expected, null, 2)}\nActual: ${JSON.stringify(actual, null, 2)}`, ); } } /** * Validate output format */ validateFormat(output: unknown, format: 'json' | 'text' | 'csv' | 'xml' | 'yaml'): void { switch (format) { case 'json': this.validateJsonFormat(output); break; case 'text': this.validateTextFormat(output); break; case 'csv': this.validateCsvFormat(output); break; case 'xml': this.validateXmlFormat(output); break; case 'yaml': this.validateYamlFormat(output); break; default: throw new Error(`Unknown format: ${format as string}`); } } /** * Validate MCP tool output format */ validateMCPOutput(output: unknown): void { const MCPOutputSchema = z.object({ content: z.array( z.object({ type: z.enum(['text', 'image', 'resource']), text: z.string().optional(), data: z.string().optional(), mimeType: z.string().optional(), resource: z .object({ uri: z.string(), text: z.string().optional(), mimeType: z.string().optional(), }) .optional(), }), ), isError: z.boolean().optional(), }); const result = MCPOutputSchema.safeParse(output); if (!result.success) { throw new Error(`Invalid MCP output format: ${result.error.message}`); } } /** * Validate output contains expected patterns */ validatePatterns(output: string, patterns: RegExp[]): void { for (const pattern of patterns) { if (!pattern.test(output)) { throw new Error(`Output doesn't match pattern: ${pattern}`); } } } /** * Validate output doesn't contain forbidden patterns */ validateNoForbiddenPatterns(output: string, patterns: RegExp[]): void { for (const pattern of patterns) { if (pattern.test(output)) { throw new Error(`Output contains forbidden pattern: ${pattern}`); } } } /** * Validate output structure */ validateStructure(output: unknown, structure: OutputStructure): void { if (structure.type) { const actualType = Array.isArray(output) ? 'array' : typeof output; if (actualType !== structure.type) { throw new Error(`Expected output type ${structure.type}, got ${actualType}`); } } if (structure.required && typeof output === 'object' && output !== null) { for (const field of structure.required) { if (!(field in output)) { throw new Error(`Missing required field: ${field}`); } } } if (structure.properties && typeof output === 'object' && output !== null) { for (const [key, validator] of Object.entries(structure.properties)) { const value = (output as Record<string, unknown>)[key]; if (value !== undefined) { validator(value); } } } if (structure.minLength !== undefined) { const length = Array.isArray(output) ? output.length : String(output).length; if (length < structure.minLength) { throw new Error(`Output length ${length} is less than minimum ${structure.minLength}`); } } if (structure.maxLength !== undefined) { const length = Array.isArray(output) ? output.length : String(output).length; if (length > structure.maxLength) { throw new Error(`Output length ${length} is greater than maximum ${structure.maxLength}`); } } } /** * Validate JSON format */ private validateJsonFormat(output: unknown): void { if (typeof output === 'string') { try { JSON.parse(output); } catch { throw new Error('Output is not valid JSON'); } } else if (typeof output !== 'object' || output === null) { throw new Error('Output is not a JSON object'); } } /** * Validate text format */ private validateTextFormat(output: unknown): void { if (typeof output !== 'string') { throw new Error('Output is not text'); } } /** * Validate CSV format */ private validateCsvFormat(output: unknown): void { if (typeof output !== 'string') { throw new Error('Output is not CSV text'); } // Basic CSV validation - check for delimiter consistency const lines = output.split('\n').filter(Boolean); if (lines.length > 0) { const firstLine = lines[0]; if (!firstLine) { throw new Error('Empty first line in CSV'); } const firstLineCommas = (firstLine.match(/,/g) || []).length; for (const line of lines) { const commas = (line.match(/,/g) || []).length; if (commas !== firstLineCommas) { throw new Error('Inconsistent CSV column count'); } } } } /** * Validate XML format */ private validateXmlFormat(output: unknown): void { if (typeof output !== 'string') { throw new Error('Output is not XML text'); } // Basic XML validation - check for balanced tags const openTags = output.match(/<[^/][^>]*>/g) || []; const closeTags = output.match(/<\/[^>]+>/g) || []; if (openTags.length !== closeTags.length) { throw new Error('Unbalanced XML tags'); } } /** * Validate YAML format */ private validateYamlFormat(output: unknown): void { if (typeof output !== 'string') { throw new Error('Output is not YAML text'); } // Basic YAML validation - check for proper indentation const lines = output.split('\n'); for (const line of lines) { if (line.trim() && !line.match(/^(\s*)(\S.*)?$/)) { throw new Error('Invalid YAML indentation'); } } } /** * Check if value is a Zod schema */ private isZodSchema(value: unknown): value is ZodSchema { return ( typeof value === 'object' && value !== null && 'safeParse' in value && typeof (value as { safeParse?: unknown }).safeParse === 'function' ); } /** * Deep equality check */ private deepEqual(a: unknown, b: unknown): boolean { if (a === b) return true; if (typeof a !== typeof b) return false; if (a === null || b === null) return false; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; return a.every((val, idx) => this.deepEqual(val, b[idx])); } if (typeof a === 'object' && typeof b === 'object') { const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) return false; return aKeys.every((key) => this.deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]), ); } return false; } } /** * Output structure definition */ export interface OutputStructure { type?: 'string' | 'number' | 'boolean' | 'array' | 'object'; required?: string[]; properties?: Record<string, (value: unknown) => void>; minLength?: number; maxLength?: number; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/conorluddy/ContextPods'

If you have feedback or need assistance with the MCP directory API, please join our Discord server