Skip to main content
Glama
tacticalBoard.js33.4 kB
import { BISHOP, Chess, KING, KNIGHT, PAWN, QUEEN, ROOK, } from "chess.js"; var PieceType; (function (PieceType) { PieceType["Pawn"] = "P"; PieceType["Knight"] = "N"; PieceType["Bishop"] = "B"; PieceType["Rook"] = "R"; PieceType["Queen"] = "Q"; PieceType["King"] = "K"; PieceType["None"] = ""; })(PieceType || (PieceType = {})); var PieceColour; (function (PieceColour) { PieceColour["White"] = "W"; PieceColour["Black"] = "B"; })(PieceColour || (PieceColour = {})); export class TacticalBoard { board = Array.from({ length: 8 }, () => Array(8).fill("")); fen = ""; squaresAttackedByWhite = Array.from({ length: 8 }, () => Array(8).fill(0)); squaresAttackedByBlack = Array.from({ length: 8 }, () => Array(8).fill(0)); hangingPieceDescriptions = []; hangingPieceCoordinates = []; semiProtectedPieceDescriptions = []; semiProtectedPieceCoordinates = []; whitePins = []; blackPins = []; // note that 0,0 is the top left constructor(fen) { this.parseFEN(fen); this.fen = fen; this.calculateDefendersAndAttackers(); this.calculatePieceVulnerability(); this.detectPins(); } get HangingPieceDescriptions() { return this.hangingPieceDescriptions; } get HangingPieceCoordinates() { return this.hangingPieceCoordinates; } get SemiProtectedPieceDescriptions() { return this.semiProtectedPieceDescriptions; } get SemiProtectedPieceCoordinates() { return this.semiProtectedPieceCoordinates; } toString() { const lines = []; lines.push("CHESS POSITION TACTICAL ANALYSIS:"); lines.push(""); lines.push("=== PIECE VULNERABILITY ==="); lines.push("HANGING PIECES (Undefended and Attacked):"); lines.push("Definition: Pieces attacked by opponent with zero defenders - can be captured for free."); if (this.hangingPieceDescriptions.length > 0) { for (let i = 0; i < this.hangingPieceDescriptions.length; i++) { lines.push(`• ${this.hangingPieceDescriptions[i]} at ${this.hangingPieceCoordinates[i]} - IMMEDIATE THREAT`); } } else { lines.push("• No hanging pieces detected"); } lines.push(""); lines.push("SEMI-PROTECTED PIECES (Equal Attackers/Defenders):"); lines.push("Definition: Pieces where attackers equal defenders - captures lead to equal material trades."); if (this.semiProtectedPieceDescriptions.length > 0) { for (let i = 0; i < this.semiProtectedPieceDescriptions.length; i++) { lines.push(`• ${this.semiProtectedPieceDescriptions[i]} at ${this.semiProtectedPieceCoordinates[i]} - CONTESTED`); } } else { lines.push("• No semi-protected pieces detected"); } lines.push(""); lines.push("=== PIN DETECTION ==="); lines.push("ABSOLUTE PINS: Pieces pinned to the King - cannot move without exposing King to check"); lines.push("RELATIVE PINS: Pieces pinned to more valuable pieces - moving loses material advantage"); lines.push(""); lines.push("WHITE PINS (White pieces pinning Black):"); if (this.whitePins.length > 0) { for (const pin of this.whitePins) { const pinType = pin.isAbsolute ? "ABSOLUTE PIN" : "RELATIVE PIN"; lines.push(`• ${pinType}: ${pin.pinningPiece} at ${pin.pinningSquare} pins ${pin.pinnedPiece} at ${pin.pinnedSquare} to ${pin.targetPiece} at ${pin.targetSquare}`); } } else { lines.push("• No pins by White detected"); } lines.push(""); lines.push("BLACK PINS (Black pieces pinning White):"); if (this.blackPins.length > 0) { for (const pin of this.blackPins) { const pinType = pin.isAbsolute ? "ABSOLUTE PIN" : "RELATIVE PIN"; lines.push(`• ${pinType}: ${pin.pinningPiece} at ${pin.pinningSquare} pins ${pin.pinnedPiece} at ${pin.pinnedSquare} to ${pin.targetPiece} at ${pin.targetSquare}`); } } else { lines.push("• No pins by Black detected"); } lines.push(""); lines.push("=== FORK OPPORTUNITIES ==="); lines.push("DEADLY FORKS: Attack 2+ pieces where at least one is higher value OR undefended (profitable capture guaranteed)"); lines.push("REGULAR FORKS: Attack 2+ pieces but exchanges may not be favorable (positional pressure)"); lines.push(""); lines.push("WHITE FORKS:"); const whiteForksBefore = lines.length; this.printAllForks(lines, "w"); if (lines.length === whiteForksBefore) { lines.push("• No white fork opportunities detected"); } lines.push(""); lines.push("BLACK FORKS:"); const blackForksBefore = lines.length; this.printAllForks(lines, "b"); if (lines.length === blackForksBefore) { lines.push("• No black fork opportunities detected"); } lines.push(""); lines.push("=== TACTICAL SUMMARY ==="); lines.push("PRIORITY ACTIONS:"); lines.push("1. Immediately address any hanging pieces (move or defend them)"); lines.push("2. Be aware of absolute pins - these pieces cannot move"); lines.push("3. Execute deadly fork opportunities when available"); lines.push("4. Exploit relative pins for tactical advantage"); lines.push("5. Monitor semi-protected pieces for tactical combinations"); lines.push("6. Look for counter-tactics against opponent fork threats"); return lines.join("\n"); } detectPins() { // Check for pins by white pieces (pinning black pieces) this.detectPinsForColor(PieceColour.White); // Check for pins by black pieces (pinning white pieces) this.detectPinsForColor(PieceColour.Black); } detectPinsForColor(attackingColor) { const pins = []; // Look for all sliding pieces (bishops, rooks, queens) of the attacking color for (let y = 0; y < 8; y++) { for (let x = 0; x < 8; x++) { const [colour, piece] = this.getPieceAt(x, y); // Only check sliding pieces that can create pins if (colour === attackingColor && (piece === PieceType.Bishop || piece === PieceType.Rook || piece === PieceType.Queen)) { // Get the square notation const attackingSquare = this.coordsToSquare(x, y); // Check each direction this piece can attack const directions = this.getDirectionsForPiece(piece); for (const [dx, dy] of directions) { const pinResult = this.checkDirectionForPin(x, y, dx, dy, attackingColor); if (pinResult) { pins.push({ pinnedPiece: pinResult.pinnedPiece, pinnedSquare: pinResult.pinnedSquare, pinningPiece: `${this.getColor(attackingColor)} ${this.getPieceMap(piece)}`, pinningSquare: attackingSquare, targetPiece: pinResult.targetPiece, targetSquare: pinResult.targetSquare, isAbsolute: pinResult.isAbsolute, }); } } } } } // Store the pins in the appropriate array if (attackingColor === PieceColour.White) { this.whitePins = pins; } else { this.blackPins = pins; } } checkDirectionForPin(x, y, dx, dy, attackingColor) { const enemyColor = attackingColor === PieceColour.White ? PieceColour.Black : PieceColour.White; let firstPiece = null; let secondPiece = null; let currentX = x + dx; let currentY = y + dy; // Travel along the direction looking for pieces while (this.isInBoard(currentX, currentY)) { const [color, piece] = this.getPieceAt(currentX, currentY); if (piece !== PieceType.None) { if (!firstPiece) { firstPiece = { piece, color, x: currentX, y: currentY }; } else if (!secondPiece) { secondPiece = { piece, color, x: currentX, y: currentY }; break; // Found two pieces, stop searching } } currentX += dx; currentY += dy; } // Check if we have a pin situation if (firstPiece && secondPiece) { // The first piece must be enemy color and the second piece must also be enemy color if (firstPiece.color === enemyColor && secondPiece.color === enemyColor) { const firstPieceValue = this.getPieceValueByType(firstPiece.piece); const secondPieceValue = this.getPieceValueByType(secondPiece.piece); // Check if it's an absolute pin (pinned to king) const isAbsolute = secondPiece.piece === PieceType.King; // For relative pins, the piece behind must be more valuable // For absolute pins, always report if (isAbsolute || secondPieceValue > firstPieceValue) { return { pinnedPiece: `${this.getColor(firstPiece.color)} ${this.getPieceMap(firstPiece.piece)}`, pinnedSquare: this.coordsToSquare(firstPiece.x, firstPiece.y), targetPiece: `${this.getColor(secondPiece.color)} ${this.getPieceMap(secondPiece.piece)}`, targetSquare: this.coordsToSquare(secondPiece.x, secondPiece.y), isAbsolute: isAbsolute, }; } } } return null; } getDirectionsForPiece(piece) { switch (piece) { case PieceType.Bishop: return [ [-1, -1], [-1, 1], [1, -1], [1, 1], ]; case PieceType.Rook: return [ [-1, 0], [1, 0], [0, -1], [0, 1], ]; case PieceType.Queen: return [ [-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1], ]; default: return []; } } coordsToSquare(x, y) { const file = String.fromCharCode(x + "a".charCodeAt(0)); const rank = String.fromCharCode(7 - y + "1".charCodeAt(0)); return `${file}${rank}`; } getPieceValueByType(piece) { switch (piece) { case PieceType.Pawn: return 1; case PieceType.Knight: case PieceType.Bishop: return 3; case PieceType.Rook: return 5; case PieceType.Queen: return 8; case PieceType.King: return 1000; default: return 0; } } getPieceMap(p) { switch (p) { case PieceType.Bishop: return "bishop"; case PieceType.Knight: return "knight"; case PieceType.Pawn: return "pawn"; case PieceType.Queen: return "queen"; case PieceType.Rook: return "rook"; case PieceType.King: return "king"; } return ""; } getColor(p) { if (p === PieceColour.White) { return "white"; } return "black"; } calculatePieceVulnerability() { for (let y = 0; y < 8; y++) { for (let x = 0; x < 8; x++) { const [colour, piece] = this.getPieceAt(x, y); if (piece !== PieceType.None && piece !== PieceType.King) { const defended = colour === PieceColour.White ? this.squaresAttackedByWhite : this.squaresAttackedByBlack; const attacked = colour === PieceColour.Black ? this.squaresAttackedByWhite : this.squaresAttackedByBlack; const attackers = attacked[x][y]; const defenders = defended[x][y]; const xcoord = String.fromCharCode(x + "a".charCodeAt(0)); const ycoord = String.fromCharCode(7 - y + "1".charCodeAt(0)); const coord = `${xcoord}${ycoord}`; const pieceDescription = `${this.getColor(colour)} ${this.getPieceMap(piece)}`; if (attackers > defenders && defenders === 0) { this.hangingPieceDescriptions.push(pieceDescription); this.hangingPieceCoordinates.push(coord); } else if (attackers === defenders && attackers > 0) { this.semiProtectedPieceDescriptions.push(pieceDescription); this.semiProtectedPieceCoordinates.push(coord); } } } } } isInBoard(x, y) { return x >= 0 && x <= 7 && y >= 0 && y <= 7; } getPieceType(piece) { return piece.toUpperCase(); } getPieceColour(piece) { return piece === piece.toUpperCase() ? PieceColour.White : PieceColour.Black; } getPieceAt(x, y) { const piece = this.board[x][y]; return [this.getPieceColour(piece), this.getPieceType(piece)]; } addAttackedSquare(squares, x, y) { if (this.isInBoard(x, y)) { squares[x][y]++; } } addAttackedDiagonals(squares, colour, x, y) { this.addAttackedDiagonalOrLine(squares, colour, x, y, -1, -1); this.addAttackedDiagonalOrLine(squares, colour, x, y, 1, -1); this.addAttackedDiagonalOrLine(squares, colour, x, y, 1, 1); this.addAttackedDiagonalOrLine(squares, colour, x, y, -1, 1); } addAttackedRanksAndFiles(squares, colour, x, y) { this.addAttackedDiagonalOrLine(squares, colour, x, y, -1, 0); this.addAttackedDiagonalOrLine(squares, colour, x, y, 1, 0); this.addAttackedDiagonalOrLine(squares, colour, x, y, 0, 1); this.addAttackedDiagonalOrLine(squares, colour, x, y, 0, -1); } addAttackedDiagonalOrLine(squares, colour, x, y, dx, dy) { let i = x; let j = y; let xrays; if (dx === 0 || dy === 0) { // can x-ray through rooks and queen of same colour xrays = colour === PieceColour.White ? ["R", "Q"] : ["r", "q"]; } else { // can x-ray through bishops and queen of same colour xrays = colour === PieceColour.White ? ["B", "Q"] : ["b", "q"]; } while (true) { i += dx; j += dy; if (!this.isInBoard(i, j)) { break; } squares[i][j]++; if (!this.board[i][j]) { // there is no piece on the square, so continue until the end of the board continue; } if (xrays.includes(this.board[i][j])) { // can x-ray right through this continue; } // got to stop now break; } } calculateDefendersAndAttackers() { // starting top left for (let y = 0; y < 8; y++) { for (let x = 0; x < 8; x++) { const [colour, piece] = this.getPieceAt(x, y); const squares = colour === PieceColour.White ? this.squaresAttackedByWhite : this.squaresAttackedByBlack; switch (piece) { case PieceType.Pawn: const dir = colour === PieceColour.White ? -1 : 1; this.addAttackedSquare(squares, x - 1, y + dir); this.addAttackedSquare(squares, x + 1, y + dir); break; case PieceType.Knight: const knightMoves = [ [-2, -1], [-2, 1], [-1, -2], [1, -2], [2, -1], [2, 1], [-1, 2], [1, 2], ]; for (const [dx, dy] of knightMoves) { this.addAttackedSquare(squares, x + dx, y + dy); } break; case PieceType.Bishop: this.addAttackedDiagonals(squares, colour, x, y); break; case PieceType.Rook: this.addAttackedRanksAndFiles(squares, colour, x, y); break; case PieceType.Queen: this.addAttackedDiagonals(squares, colour, x, y); this.addAttackedRanksAndFiles(squares, colour, x, y); break; case PieceType.King: for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { if (dx !== 0 || dy !== 0) { this.addAttackedSquare(squares, x + dx, y + dy); } } } break; } } } } getPieceValue(piece) { switch (piece) { case "p": return 1; case "b": return 3; case "n": return 3; case "r": return 5; case "q": return 8; case "k": return 1000; } } getPieceName(piece) { switch (piece) { case "p": return "pawn"; case "b": return "bishop"; case "n": return "knight"; // Fixed typo: was "night" case "r": return "rook"; case "q": return "queen"; case "k": return "king"; } } calculateTotalFork(piece, side) { const pieceForks = []; // Create a new chess instance for this calculation const chess = new Chess(this.fen); const pieces = chess.findPiece(piece); if (pieces.length === 0) { return []; } for (let i = 0; i < pieces.length; i++) { const p = pieces[i]; // Don't set turn - just pass the chess instance and side const forks = this.calculateFork(piece.type, p, side, chess); if (forks.length > 0) { pieceForks.push(forks); } } return pieceForks; } printPieceForks(piece, lines, side) { const calTf = this.calculateTotalFork(piece, side); for (let i = 0; i < calTf.length; i++) { for (let j = 0; j < calTf[i].length; j++) { lines.push(`• ${calTf[i][j]}`); } } } printAllForks(lines, side) { const pawn = { color: side, type: PAWN, }; const bishop = { color: side, type: BISHOP, }; const knight = { color: side, type: KNIGHT, }; const rook = { color: side, type: ROOK, }; const queen = { color: side, type: QUEEN, }; const king = { color: side, type: KING, }; this.printPieceForks(pawn, lines, side); this.printPieceForks(knight, lines, side); this.printPieceForks(bishop, lines, side); this.printPieceForks(rook, lines, side); this.printPieceForks(queen, lines, side); this.printPieceForks(king, lines, side); } calculateFork(piece, pieceSq, side, chess) { const forks = []; // Get all squares this piece can attack from its current position // Don't use chess.moves() as it's restricted when king is in check const attackedSquares = this.getAttackedSquares(piece, pieceSq, chess); // Find all enemy pieces that this piece can attack const attackableTargets = []; const attackingPieceValue = this.getPieceValue(piece); for (const square of attackedSquares) { const pieceOnSquare = chess.get(square); if (pieceOnSquare && pieceOnSquare.color !== side) { const targetValue = this.getPieceValue(pieceOnSquare.type); // Check if target square is defended by the enemy const defended = side === "w" ? this.squaresAttackedByBlack : this.squaresAttackedByWhite; const file = square.charCodeAt(0) - "a".charCodeAt(0); const rank = parseInt(square[1]) - 1; const isDefended = defended[file][7 - rank] > 0; // Note: board coordinates are flipped // A target is "deadly" if: // 1. It's higher value than the attacking piece, OR // 2. It's undefended (even if lower value, it's a free capture) const isDeadly = targetValue > attackingPieceValue || !isDefended; attackableTargets.push({ square: square, piece: pieceOnSquare.type, isDeadly: isDeadly, }); } } // Filter to only deadly targets const deadlyTargets = attackableTargets.filter((target) => target.isDeadly); // A deadly fork occurs when a piece can attack 2 or more deadly targets simultaneously if (deadlyTargets.length >= 2) { const targetDescriptions = deadlyTargets .map((target) => { const value = this.getPieceValue(target.piece); const defended = attackableTargets.find((t) => t.square === target.square)?.isDeadly ? value > attackingPieceValue ? "(higher value)" : "(undefended)" : ""; return `${this.getPieceName(target.piece)} on ${target.square} ${defended}`; }) .join(", "); forks.push(`DEADLY FORK: The ${this.getPieceName(piece)} at ${pieceSq} is forking: ${targetDescriptions}`); } // Also report regular forks (attacking 2+ pieces) but mark them as less critical else if (attackableTargets.length >= 2) { const targetDescriptions = attackableTargets .map((target) => `${this.getPieceName(target.piece)} on ${target.square}`) .join(", "); forks.push(`Regular fork: The ${this.getPieceName(piece)} at ${pieceSq} is attacking: ${targetDescriptions} (but exchange may not be favorable)`); } return forks; } getAttackedSquares(piece, square, chess) { const attackedSquares = []; // Convert square notation to coordinates const file = square.charCodeAt(0) - "a".charCodeAt(0); const rank = parseInt(square[1]) - 1; switch (piece) { case "p": // pawn const pieceColor = chess.get(square)?.color; const direction = pieceColor === "w" ? 1 : -1; const newRank = rank + direction; if (newRank >= 0 && newRank <= 7) { if (file > 0) { attackedSquares.push(`${String.fromCharCode("a".charCodeAt(0) + file - 1)}${newRank + 1}`); } if (file < 7) { attackedSquares.push(`${String.fromCharCode("a".charCodeAt(0) + file + 1)}${newRank + 1}`); } } break; case "n": // knight const knightMoves = [ [-2, -1], [-2, 1], [-1, -2], [1, -2], [2, -1], [2, 1], [-1, 2], [1, 2], ]; for (const [df, dr] of knightMoves) { const newFile = file + df; const newRank = rank + dr; if (newFile >= 0 && newFile <= 7 && newRank >= 0 && newRank <= 7) { attackedSquares.push(`${String.fromCharCode("a".charCodeAt(0) + newFile)}${newRank + 1}`); } } break; case "b": // bishop this.addSlidingAttacks(attackedSquares, file, rank, [ [-1, -1], [-1, 1], [1, -1], [1, 1], ], chess); break; case "r": // rook this.addSlidingAttacks(attackedSquares, file, rank, [ [-1, 0], [1, 0], [0, -1], [0, 1], ], chess); break; case "q": // queen this.addSlidingAttacks(attackedSquares, file, rank, [ [-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1], ], chess); break; case "k": // king for (let df = -1; df <= 1; df++) { for (let dr = -1; dr <= 1; dr++) { if (df === 0 && dr === 0) continue; const newFile = file + df; const newRank = rank + dr; if (newFile >= 0 && newFile <= 7 && newRank >= 0 && newRank <= 7) { attackedSquares.push(`${String.fromCharCode("a".charCodeAt(0) + newFile)}${newRank + 1}`); } } } break; } return attackedSquares; } addSlidingAttacks(attackedSquares, file, rank, directions, chess) { for (const [df, dr] of directions) { let currentFile = file; let currentRank = rank; while (true) { currentFile += df; currentRank += dr; if (currentFile < 0 || currentFile > 7 || currentRank < 0 || currentRank > 7) { break; } const targetSquare = `${String.fromCharCode("a".charCodeAt(0) + currentFile)}${currentRank + 1}`; attackedSquares.push(targetSquare); // Stop if there's a piece on this square const pieceOnSquare = chess.get(targetSquare); if (pieceOnSquare) { break; } } } } parseFEN(fen) { let rank = 0; let file = 0; for (let i = 0; i < fen.length; i++) { const char = fen[i]; if (char > "0" && char <= "8") { const blankCount = parseInt(char, 10); file += blankCount; } else if (char === "/") { rank++; file = 0; } else { if (char === " ") break; this.board[file][rank] = char; file++; } } } /** * Calculate a tactical score for the given side. * Higher score = better tactical position * Lower score = more tactical weaknesses * * Scoring system: * - Hanging pieces: -10 points each (critical weakness) * - Semi-protected pieces: -3 points each (vulnerable) * - Pins created by this side: +5 points for absolute, +3 for relative * - Pins against this side: -5 points for absolute, -3 for relative * - Deadly forks available: +8 points each * - Regular forks available: +4 points each * - Opponent's deadly forks: -8 points each * - Opponent's regular forks: -4 points each */ calculateTacticalScore(side) { let score = 0; // 1. Evaluate hanging pieces (most critical weakness) for (let i = 0; i < this.hangingPieceDescriptions.length; i++) { const pieceDesc = this.hangingPieceDescriptions[i]; const belongsToSide = (side === "w" && pieceDesc.includes("white")) || (side === "b" && pieceDesc.includes("black")); if (belongsToSide) { score -= 10; // Our hanging piece = bad } else { score += 10; // Opponent's hanging piece = good for us } } // 2. Evaluate semi-protected pieces (moderate weakness) for (let i = 0; i < this.semiProtectedPieceDescriptions.length; i++) { const pieceDesc = this.semiProtectedPieceDescriptions[i]; const belongsToSide = (side === "w" && pieceDesc.includes("white")) || (side === "b" && pieceDesc.includes("black")); if (belongsToSide) { score -= 3; // Our semi-protected piece = slight weakness } else { score += 3; // Opponent's semi-protected piece = slight advantage } } // 3. Evaluate pins created by our side (tactical advantage) const ourPins = side === "w" ? this.whitePins : this.blackPins; for (const pin of ourPins) { if (pin.isAbsolute) { score += 5; // Absolute pin is very strong } else { score += 3; // Relative pin is moderately strong } } // 4. Evaluate pins against our side (tactical weakness) const opponentPins = side === "w" ? this.blackPins : this.whitePins; for (const pin of opponentPins) { if (pin.isAbsolute) { score -= 5; // Being absolutely pinned is very bad } else { score -= 3; // Being relatively pinned is moderately bad } } // 5. Evaluate fork opportunities const pieces = [ { color: side, type: PAWN }, { color: side, type: KNIGHT }, { color: side, type: BISHOP }, { color: side, type: ROOK }, { color: side, type: QUEEN }, { color: side, type: KING }, ]; // Count our forks for (const piece of pieces) { const forkResults = this.calculateTotalFork(piece, side); for (const forkList of forkResults) { for (const fork of forkList) { if (fork.includes("DEADLY FORK")) { score += 8; // Deadly fork is very valuable } else if (fork.includes("Regular fork")) { score += 4; // Regular fork has moderate value } } } } // Count opponent's forks const opponentSide = side === "w" ? "b" : "w"; const opponentPieces = [ { color: opponentSide, type: PAWN }, { color: opponentSide, type: KNIGHT }, { color: opponentSide, type: BISHOP }, { color: opponentSide, type: ROOK }, { color: opponentSide, type: QUEEN }, { color: opponentSide, type: KING }, ]; for (const piece of opponentPieces) { const forkResults = this.calculateTotalFork(piece, opponentSide); for (const forkList of forkResults) { for (const fork of forkList) { if (fork.includes("DEADLY FORK")) { score -= 8; // Opponent's deadly fork is very dangerous } else if (fork.includes("Regular fork")) { score -= 4; // Opponent's regular fork is moderately dangerous } } } } return score; } }

Latest Blog Posts

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/jalpp/chessagine-mcp'

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