chess-trainer-mcp
Version:
Chess Trainer MCP Server with Stockfish NNUE engine - Interactive chess training with AI coaching capabilities
287 lines (248 loc) • 7.85 kB
JavaScript
import Ajv from 'ajv';
const ajv = new Ajv();
// WebSocket message schemas
const messageSchemas = {
join_session: {
type: 'object',
properties: {
type: { type: 'string', const: 'join_session' },
clientId: { type: 'string', minLength: 1, maxLength: 100 },
clientName: { type: 'string', minLength: 1, maxLength: 50 }
},
required: ['type', 'clientId', 'clientName'],
additionalProperties: false
},
move: {
type: 'object',
properties: {
type: { type: 'string', const: 'move' },
sessionId: { type: 'string', minLength: 1, maxLength: 50 },
move: {
type: 'object',
properties: {
san: { type: 'string', minLength: 2, maxLength: 8 },
uci: { type: 'string', minLength: 4, maxLength: 5 }
},
required: ['san', 'uci'],
additionalProperties: false
},
fenBefore: { type: 'string', minLength: 10, maxLength: 100 },
fenAfter: { type: 'string', minLength: 10, maxLength: 100 },
evalCp: { type: 'number', minimum: -32000, maximum: 32000 },
depth: { type: 'number', minimum: 1, maximum: 50 },
timeMs: { type: 'number', minimum: 0, maximum: 300000 }
},
required: ['type', 'sessionId', 'move', 'fenBefore', 'fenAfter', 'evalCp', 'depth'],
additionalProperties: false
},
request_analysis: {
type: 'object',
properties: {
type: { type: 'string', const: 'request_analysis' },
sessionId: { type: 'string', minLength: 1, maxLength: 50 },
fen: { type: 'string', minLength: 10, maxLength: 100 },
depth: { type: 'number', minimum: 1, maximum: 30 }
},
required: ['type', 'sessionId', 'fen'],
additionalProperties: false
},
end_session: {
type: 'object',
properties: {
type: { type: 'string', const: 'end_session' },
result: {
type: 'object',
properties: {
winner: { type: 'string', enum: ['white', 'black', 'draw'] },
reason: { type: 'string', enum: ['checkmate', 'stalemate', 'resignation', 'time', 'agreement', 'repetition', 'insufficient_material'] }
},
required: ['winner', 'reason'],
additionalProperties: false
}
},
required: ['type', 'result'],
additionalProperties: false
},
update_game_mode: {
type: 'object',
properties: {
type: { type: 'string', const: 'update_game_mode' },
sessionId: { type: 'string', minLength: 1, maxLength: 50 },
gameMode: { type: 'string', enum: ['human_vs_human', 'human_vs_ai'] },
playerColor: { type: 'string', enum: ['white', 'black'] },
aiEloRating: { type: 'number', minimum: 800, maximum: 3200 },
aiTimeLimit: { type: 'number', minimum: 100, maximum: 30000 }
},
required: ['type', 'sessionId', 'gameMode'],
additionalProperties: false
},
sync_move: {
type: 'object',
properties: {
type: { type: 'string', const: 'sync_move' },
clientId: { type: 'string', minLength: 1, maxLength: 100 },
clientName: { type: 'string', minLength: 1, maxLength: 50 },
move: {
type: 'object',
properties: {
san: { type: 'string', minLength: 2, maxLength: 8 },
uci: { type: 'string', minLength: 4, maxLength: 5 }
},
required: ['san', 'uci'],
additionalProperties: false
},
fen: { type: 'string', minLength: 10, maxLength: 100 },
turn: { type: 'string', enum: ['white', 'black'] }
},
required: ['type', 'clientId', 'clientName', 'move', 'fen', 'turn'],
additionalProperties: false
},
reset_game: {
type: 'object',
properties: {
type: { type: 'string', const: 'reset_game' },
clientId: { type: 'string', minLength: 1, maxLength: 100 },
clientName: { type: 'string', minLength: 1, maxLength: 50 },
gameSettings: {
type: 'object',
properties: {
mode: { type: 'string', enum: ['human_vs_human', 'human_vs_ai'] },
playerColor: { type: 'string', enum: ['white', 'black'] },
aiEloRating: { type: 'number', minimum: 800, maximum: 3200 },
aiTimeLimit: { type: 'number', minimum: 100, maximum: 30000 }
},
additionalProperties: false
}
},
required: ['type'],
additionalProperties: false
}
};
// Compile validators
const validators = {};
for (const [messageType, schema] of Object.entries(messageSchemas)) {
validators[messageType] = ajv.compile(schema);
}
export function validateMessage(message) {
if (!message || typeof message !== 'object') {
return false;
}
const { type } = message;
if (!type || typeof type !== 'string') {
return false;
}
const validator = validators[type];
if (!validator) {
console.warn(`Unknown message type: ${type}`);
return false;
}
const isValid = validator(message);
if (!isValid) {
console.warn(`Invalid message format for type ${type}:`, validator.errors);
}
return isValid;
}
// Additional validation functions
export function validateFEN(fen) {
if (!fen || typeof fen !== 'string') {
return false;
}
const parts = fen.split(' ');
if (parts.length !== 6) {
return false;
}
const [position, activeColor, castling, enPassant, halfmove, fullmove] = parts;
// Basic position validation
const ranks = position.split('/');
if (ranks.length !== 8) {
return false;
}
for (const rank of ranks) {
let squares = 0;
for (const char of rank) {
if ('12345678'.includes(char)) {
squares += parseInt(char);
} else if ('pnbrqkPNBRQK'.includes(char)) {
squares += 1;
} else {
return false;
}
}
if (squares !== 8) {
return false;
}
}
// Active color validation
if (!['w', 'b'].includes(activeColor)) {
return false;
}
// Castling validation
if (!/^(-|[KQkq]+)$/.test(castling)) {
return false;
}
// En passant validation
if (!/^(-|[a-h][36])$/.test(enPassant)) {
return false;
}
// Move count validation
if (!/^\d+$/.test(halfmove) || !/^\d+$/.test(fullmove)) {
return false;
}
return true;
}
export function validateUCI(uci) {
if (!uci || typeof uci !== 'string') {
return false;
}
// Standard move: e2e4, g1f3, etc.
if (/^[a-h][1-8][a-h][1-8]$/.test(uci)) {
return true;
}
// Promotion: e7e8q, a2a1n, etc.
if (/^[a-h][1-8][a-h][1-8][qrbn]$/.test(uci)) {
return true;
}
return false;
}
export function validateSAN(san) {
if (!san || typeof san !== 'string') {
return false;
}
// Basic SAN patterns - this is simplified
const sanPatterns = [
/^[NBRQK][a-h]?[1-8]?x?[a-h][1-8](\+|#)?$/, // Piece moves
/^[a-h][1-8](\+|#)?$/, // Pawn moves
/^[a-h]x[a-h][1-8](\+|#)?$/, // Pawn captures
/^[a-h][1-8]=[QRBN](\+|#)?$/, // Pawn promotion
/^[a-h]x[a-h][1-8]=[QRBN](\+|#)?$/, // Pawn capture promotion
/^O-O(\+|#)?$/, // Kingside castling
/^O-O-O(\+|#)?$/ // Queenside castling
];
return sanPatterns.some(pattern => pattern.test(san));
}
export function validateSessionId(sessionId) {
if (!sessionId || typeof sessionId !== 'string') {
return false;
}
// Allow alphanumeric session IDs with underscores and hyphens
return /^[a-zA-Z0-9_-]{1,50}$/.test(sessionId);
}
export function sanitizeString(str, maxLength = 100) {
if (!str || typeof str !== 'string') {
return '';
}
return str.trim().slice(0, maxLength);
}
export function validateEvaluation(evalCp) {
if (typeof evalCp !== 'number') {
return false;
}
// Reasonable evaluation bounds (-32000 to +32000 centipawns)
return evalCp >= -32000 && evalCp <= 32000;
}
export function validateDepth(depth) {
if (typeof depth !== 'number' || !Number.isInteger(depth)) {
return false;
}
return depth >= 1 && depth <= 50;
}