mcp-quickbase
Version:
Work with Quickbase via Model Context Protocol
230 lines • 7.73 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createZodObjectSchema = exports.createZodSchema = exports.ValidationError = void 0;
exports.createMcpZodSchema = createMcpZodSchema;
exports.createValidationSchema = createValidationSchema;
exports.validateParams = validateParams;
const zod_1 = require("zod");
const logger_1 = require("./logger");
const logger = (0, logger_1.createLogger)("Validation");
/**
* Custom validation error with additional context
*/
class ValidationError extends Error {
constructor(message, context) {
super(message);
this.context = context;
this.name = "ValidationError";
}
}
exports.ValidationError = ValidationError;
/**
* Type guard for valid JSON Schema objects
*/
function isValidJSONSchema(schema) {
return typeof schema === "object" && schema !== null && "type" in schema;
}
/**
* Schema cache for performance optimization
*/
class SchemaCache {
static get(key) {
const value = this.cache.get(key);
if (value !== undefined) {
// Move to end for LRU behavior
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
static set(key, schema) {
// Remove existing key if present (for LRU update)
if (this.cache.has(key)) {
this.cache.delete(key);
}
// Evict oldest entries if cache is full (proper LRU eviction)
while (this.cache.size >= this.MAX_SIZE) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
else {
break; // Safety check
}
}
this.cache.set(key, schema);
}
static clear() {
this.cache.clear();
}
static getStats() {
return { size: this.cache.size, maxSize: this.MAX_SIZE };
}
}
SchemaCache.cache = new Map();
SchemaCache.MAX_SIZE = 100;
/**
* Convert JSON Schema property to Zod type
*/
function convertPropertyToZod(prop) {
if (!prop || typeof prop !== "object") {
logger.warn("Invalid property schema, defaulting to unknown", { prop });
return zod_1.z.unknown();
}
const { type, enum: enumValues, items, format } = prop;
// Handle enum constraints
if (enumValues && Array.isArray(enumValues) && enumValues.length > 0) {
if (enumValues.every((v) => typeof v === "string")) {
return zod_1.z.enum(enumValues);
}
const literals = enumValues.map((v) => zod_1.z.literal(v));
if (literals.length === 1) {
return literals[0];
}
return zod_1.z.union([literals[0], literals[1], ...literals.slice(2)]);
}
// Handle primitive types
switch (type) {
case "string": {
let stringSchema = zod_1.z.string();
if (format === "email")
stringSchema = stringSchema.email();
if (format === "uri")
stringSchema = stringSchema.url();
return stringSchema;
}
case "number":
return zod_1.z.number();
case "integer":
return zod_1.z.number().int();
case "boolean":
return zod_1.z.boolean();
case "array":
if (items && typeof items === "object") {
const itemSchema = convertPropertyToZod(items);
return zod_1.z.array(itemSchema);
}
return zod_1.z.array(zod_1.z.unknown());
case "object":
// For nested objects, we'd need recursive handling
// For now, treat as record of unknown values
return zod_1.z.record(zod_1.z.unknown());
default:
logger.warn("Unsupported schema type, defaulting to unknown", {
type,
prop,
});
return zod_1.z.unknown();
}
}
/**
* Create Zod schema for MCP server tool registration
* Returns object with property schemas for MCP server
*/
function createMcpZodSchema(schema) {
if (!isValidJSONSchema(schema)) {
logger.warn("Invalid schema provided to createMcpZodSchema", { schema });
return {};
}
if (schema.type !== "object" || !schema.properties) {
return {};
}
const properties = schema.properties;
const required = schema.required || [];
const zodSchemaObj = {};
try {
Object.entries(properties).forEach(([key, prop]) => {
let zodType = convertPropertyToZod(prop);
// Make optional if not in required array
if (!required.includes(key)) {
zodType = zodType.optional();
}
zodSchemaObj[key] = zodType;
});
}
catch (error) {
logger.error("Error creating MCP Zod schema", { error, schema });
return {};
}
return zodSchemaObj;
}
/**
* Create Zod object schema for parameter validation
* Returns complete Zod schema for validation
*/
function createValidationSchema(schema) {
if (!isValidJSONSchema(schema)) {
logger.warn("Invalid schema provided to createValidationSchema", {
schema,
});
return zod_1.z.object({});
}
// Create cache key
const cacheKey = JSON.stringify(schema);
// Check cache first
const cached = SchemaCache.get(cacheKey);
if (cached) {
return cached;
}
let zodSchema;
try {
if (schema.type === "object" && schema.properties) {
const mcpSchema = createMcpZodSchema(schema);
zodSchema = zod_1.z.object(mcpSchema);
}
else {
// Non-object schemas (rare but possible)
zodSchema = zod_1.z.object({});
}
}
catch (error) {
logger.error("Error creating validation schema", { error, schema });
zodSchema = zod_1.z.object({});
}
// Cache the result
SchemaCache.set(cacheKey, zodSchema);
return zodSchema;
}
/**
* Validate parameters using JSON Schema with comprehensive error handling
*/
function validateParams(params, schema, toolName) {
try {
const zodSchema = createValidationSchema(schema);
logger.debug("Validating parameters", {
toolName,
paramsType: typeof params,
schemaType: schema.type,
});
const result = zodSchema.parse(params);
logger.debug("Parameter validation successful", { toolName });
return result;
}
catch (error) {
if (error instanceof zod_1.z.ZodError) {
const context = toolName ? ` in tool "${toolName}"` : "";
const issues = error.errors.map((err) => {
const path = err.path.length > 0 ? err.path.join(".") : "root";
return `${path}: ${err.message}`;
});
const validationError = new ValidationError(`Parameter validation failed${context}: ${issues.join(", ")}`, {
toolName,
issues: error.errors,
originalParams: params,
});
logger.error("Parameter validation failed", {
toolName,
error: validationError.message,
issues: error.errors,
params: typeof params === "object" ? "object" : params,
});
throw validationError;
}
logger.error("Unexpected validation error", { error, toolName });
throw error;
}
}
// Legacy exports for backward compatibility
exports.createZodSchema = createMcpZodSchema;
exports.createZodObjectSchema = createValidationSchema;
//# sourceMappingURL=validation.js.map