leshan-mcp-server
Version:
A standards-compliant MCP server for Leshan LwM2M, exposing Leshan as Model Context Protocol tools.
158 lines (139 loc) • 4.09 kB
JavaScript
import { z } from "zod";
/**
* Custom validation error class
*/
export class ValidationError extends Error {
constructor(message, field, value) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.value = value;
}
}
/**
* Device ID validation schema
*/
export const deviceIdSchema = z.string()
.min(1, "Device ID cannot be empty")
.max(64, "Device ID too long")
.regex(/^[a-zA-Z0-9._-]+$/, "Device ID contains invalid characters");
/**
* LwM2M ID validation schema (for object, instance, resource IDs)
*/
export const lwm2mIdSchema = z.string()
.regex(/^\d+$/, "Must be numeric")
.transform(val => parseInt(val, 10))
.refine(val => val >= 0 && val <= 65535, "Must be between 0 and 65535");
/**
* Resource value validation schema
*/
export const resourceValueSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.any()),
z.object({}).passthrough()
]);
/**
* Validate LwM2M path parameters
* @param {string} deviceId - Device endpoint
* @param {string} objectId - Object ID
* @param {string} instanceId - Instance ID
* @param {string} resourceId - Resource ID
* @returns {Object} Validated parameters
* @throws {ValidationError} If validation fails
*/
export function validateLwM2MPath(deviceId, objectId, instanceId, resourceId) {
try {
const validated = {
deviceId: deviceIdSchema.parse(deviceId),
objectId: lwm2mIdSchema.parse(objectId),
instanceId: lwm2mIdSchema.parse(instanceId),
resourceId: lwm2mIdSchema.parse(resourceId)
};
return validated;
} catch (error) {
if (error instanceof z.ZodError) {
const firstError = error.errors[0];
throw new ValidationError(
`Validation failed for ${firstError.path.join('.')}: ${firstError.message}`,
firstError.path.join('.'),
firstError.received
);
}
throw error;
}
}
/**
* Validate resource value
* @param {any} value - Value to validate
* @returns {any} Validated value
* @throws {ValidationError} If validation fails
*/
export function validateResourceValue(value) {
try {
return resourceValueSchema.parse(value);
} catch (error) {
if (error instanceof z.ZodError) {
throw new ValidationError(
`Invalid resource value: ${error.errors[0].message}`,
'value',
value
);
}
throw error;
}
}
/**
* Sanitize data for logging (remove sensitive information)
* @param {any} data - Data to sanitize
* @returns {any} Sanitized data
*/
export function sanitizeForLogging(data) {
if (typeof data !== 'object' || data === null) {
return data;
}
const sensitiveKeys = ['password', 'secret', 'key', 'token', 'credential'];
const sanitized = Array.isArray(data) ? [] : {};
for (const [key, value] of Object.entries(data)) {
const lowerKey = key.toLowerCase();
const isSensitive = sensitiveKeys.some(sensitive => lowerKey.includes(sensitive));
if (isSensitive) {
sanitized[key] = '[REDACTED]';
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = sanitizeForLogging(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
}
/**
* Validate and normalize endpoint path
* @param {string} endpoint - Endpoint path
* @returns {string} Normalized endpoint
*/
export function normalizeEndpoint(endpoint) {
if (!endpoint || typeof endpoint !== 'string') {
throw new ValidationError('Endpoint must be a non-empty string', 'endpoint', endpoint);
}
// Ensure endpoint starts with /
let normalized = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
// Remove double slashes
normalized = normalized.replace(/\/+/g, '/');
// Remove trailing slash unless it's the root
if (normalized.length > 1 && normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
export default {
ValidationError,
deviceIdSchema,
lwm2mIdSchema,
resourceValueSchema,
validateLwM2MPath,
validateResourceValue,
sanitizeForLogging,
normalizeEndpoint
};