Lichess MCP
by karayaman
Verified
- src
#!/usr/bin/env node
/**
* This is a Lichess MCP server that implements chess game interactions.
* It provides:
* - Listing ongoing games as resources
* - Reading game states
* - Creating challenges
* - Making moves
* - Getting game analysis
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fetch, { RequestInit, Response } from 'node-fetch';
// Import dotenv with ESM style
import 'dotenv/config';
/**
* Type definitions for Lichess data
*/
type Game = {
id: string;
status: string;
fen: string;
lastMove?: string;
players: {
white: { name: string };
black: { name: string };
};
};
type GameState = {
type: 'gameState';
moves: string;
status: string;
winner?: 'white' | 'black';
};
interface LichessResponse {
nowPlaying: Game[];
}
interface ChallengeResponse {
challenge: {
id: string;
url: string;
};
}
interface Profile {
id: string;
username: string;
perfs: {
[key: string]: {
games: number;
rating: number;
prog: number;
};
};
createdAt: number;
disabled: boolean;
tosViolation: boolean;
profile?: {
country?: string;
location?: string;
bio?: string;
firstName?: string;
lastName?: string;
fideRating?: number;
links?: string;
};
seenAt: number;
patron?: boolean;
verified: boolean;
playTime: {
total: number;
tv: number;
};
title?: string;
url: string;
playing?: string;
completionRate?: number;
count: {
all: number;
rated: number;
ai: number;
draw: number;
drawH: number;
loss: number;
lossH: number;
win: number;
winH: number;
bookmark: number;
playing: number;
import: number;
me: number;
};
streaming?: boolean;
followable: boolean;
following: boolean;
blocking: boolean;
followsYou: boolean;
}
interface EmailResponse {
email: string;
}
interface KidModeResponse {
kid: boolean;
}
interface Preferences {
dark: boolean;
transp: boolean;
bgImg: string;
is3d: boolean;
theme: string;
pieceSet: string;
theme3d: string;
pieceSet3d: string;
soundSet: string;
blindfold: number;
autoQueen: number;
autoThreefold: number;
takeback: number;
moretime: number;
clockTenths: number;
clockBar: boolean;
clockSound: boolean;
premove: boolean;
animation: number;
captured: boolean;
follow: boolean;
highlight: boolean;
destination: boolean;
coords: number;
replay: number;
challenge: number;
message: number;
coordColor: number;
submitMove: number;
confirmResign: number;
insightShare: number;
keyboardMove: number;
zen: number;
moveEvent: number;
rookCastle: number;
}
interface TimelineEntry {
type: string;
data: Record<string, any>;
date: number;
}
interface TokenTestResult {
[token: string]: {
userId: string;
scopes: string[];
} | null;
}
/**
* Lichess API configuration
*/
const LICHESS_API_URL = 'https://lichess.org/api';
let LICHESS_TOKEN: string | undefined = process.env.LICHESS_TOKEN;
const server = new Server(
{
name: "lichess-mcp",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
prompts: {},
},
}
);
/**
* Helper function to make authenticated requests to Lichess API
*/
async function lichessRequest(endpoint: string, options: RequestInit = {}): Promise<Response> {
if (!LICHESS_TOKEN) {
throw new Error('Lichess API token not set. Use the set_token tool first.');
}
const fetchOptions: RequestInit = {
...options,
headers: {
'Authorization': `Bearer ${LICHESS_TOKEN}`,
'Content-Type': 'application/json',
...(options.headers || {})
}
};
const response = await fetch(`${LICHESS_API_URL}${endpoint}`, fetchOptions);
if (!response.ok) {
throw new Error(`Lichess API error: ${response.statusText}`);
}
return response;
}
/**
* Handler that lists available tools
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "set_token",
description: "Set your Lichess API token",
inputSchema: {
type: "object",
properties: {
token: {
type: "string",
description: "Your Lichess API token"
}
},
required: ["token"]
}
},
{
name: "get_my_profile",
description: "Get your Lichess profile information",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_user_profile",
description: "Get a user's Lichess profile information",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player"
},
trophies: {
type: "boolean",
description: "Include user trophies",
default: false
}
},
required: ["username"]
}
},
{
name: "get_my_email",
description: "Get your email address",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_kid_mode",
description: "Get kid mode status",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "set_kid_mode",
description: "Set kid mode status",
inputSchema: {
type: "object",
properties: {
value: {
type: "boolean",
description: "Enable or disable kid mode"
}
},
required: ["value"]
}
},
{
name: "create_challenge",
description: "Create a new challenge",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player to challenge"
},
timeControl: {
type: "string",
description: "Time control (e.g. '10+0' for 10 minutes)",
default: "10+0"
},
color: {
type: "string",
enum: ["white", "black", "random"],
default: "random"
}
},
required: ["username"]
}
},
{
name: "make_move",
description: "Make a move in an ongoing game",
inputSchema: {
type: "object",
properties: {
gameId: {
type: "string",
description: "ID of the game"
},
move: {
type: "string",
description: "Move in UCI format (e.g. 'e2e4')"
},
offeringDraw: {
type: "boolean",
description: "Whether to offer/accept a draw",
default: false
}
},
required: ["gameId", "move"]
}
},
{
name: "get_preferences",
description: "Get your preferences",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_timeline",
description: "Get your timeline",
inputSchema: {
type: "object",
properties: {
since: {
type: "number",
description: "Show events since this timestamp"
},
nb: {
type: "number",
description: "Max number of events to fetch (1-30)",
minimum: 1,
maximum: 30,
default: 15
}
}
}
},
{
name: "test_tokens",
description: "Test multiple OAuth tokens",
inputSchema: {
type: "object",
properties: {
tokens: {
type: "string",
description: "OAuth tokens separated by commas. Up to 1000."
}
},
required: ["tokens"]
}
},
{
name: "revoke_token",
description: "Revoke the current access token",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "upgrade_to_bot",
description: "Upgrade to Bot account. WARNING: This is irreversible and the account must not have played any games.",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "add_user_note",
description: "Add a private note about a user",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player"
},
text: {
type: "string",
description: "The contents of the note"
}
},
required: ["username", "text"]
}
},
{
name: "send_message",
description: "Send a private message to another player",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the recipient"
},
text: {
type: "string",
description: "Message text"
}
},
required: ["username", "text"]
}
},
{
name: "get_following",
description: "Get users followed by the logged in user",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "follow_user",
description: "Follow a player",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player to follow"
}
},
required: ["username"]
}
},
{
name: "unfollow_user",
description: "Unfollow a player",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player to unfollow"
}
},
required: ["username"]
}
},
{
name: "block_user",
description: "Block a player",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player to block"
}
},
required: ["username"]
}
},
{
name: "get_users_status",
description: "Get real-time users status",
inputSchema: {
type: "object",
properties: {
ids: {
type: "string",
description: "User IDs separated by commas. Up to 100 IDs."
},
withSignal: {
type: "boolean",
description: "Include network signal strength (1-4)"
},
withGameIds: {
type: "boolean",
description: "Include IDs of ongoing games"
},
withGameMetas: {
type: "boolean",
description: "Include metadata of ongoing games"
}
},
required: ["ids"]
}
},
{
name: "get_all_top_10",
description: "Get the top 10 players for each speed and variant",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_leaderboard",
description: "Get the leaderboard for a single speed or variant",
inputSchema: {
type: "object",
properties: {
nb: {
type: "number",
description: "How many users to fetch (1-200)",
minimum: 1,
maximum: 200,
default: 100
},
perfType: {
type: "string",
description: "The speed or variant",
enum: ["ultraBullet", "bullet", "blitz", "rapid", "classical", "chess960", "crazyhouse", "antichess", "atomic", "horde", "kingOfTheHill", "racingKings", "threeCheck"]
}
},
required: ["perfType"]
}
},
{
name: "get_user_public_data",
description: "Get public data of a user",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player"
},
withTrophies: {
type: "boolean",
description: "Include user trophies",
default: false
}
},
required: ["username"]
}
},
{
name: "get_rating_history",
description: "Get rating history of a user for all perf types",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player"
}
},
required: ["username"]
}
},
{
name: "get_user_performance",
description: "Get performance statistics of a user",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player"
},
perf: {
type: "string",
description: "The speed or variant",
enum: ["ultraBullet", "bullet", "blitz", "rapid", "classical", "correspondence", "chess960", "crazyhouse", "antichess", "atomic", "horde", "kingOfTheHill", "racingKings", "threeCheck"]
}
},
required: ["username", "perf"]
}
},
{
name: "get_user_activity",
description: "Get activity feed of a user",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player"
}
},
required: ["username"]
}
},
{
name: "get_users_by_id",
description: "Get multiple users by their IDs",
inputSchema: {
type: "object",
properties: {
ids: {
type: "string",
description: "User IDs separated by commas. Up to 300 IDs."
}
},
required: ["ids"]
}
},
{
name: "unblock_user",
description: "Unblock a user",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player to unblock"
}
},
required: ["username"]
}
},
{
name: "export_game",
description: "Export one game in PGN or JSON format",
inputSchema: {
type: "object",
properties: {
gameId: {
type: "string",
description: "The game ID"
},
moves: {
type: "boolean",
description: "Include the PGN moves",
default: true
},
pgnInJson: {
type: "boolean",
description: "Include the full PGN within the JSON response",
default: false
},
tags: {
type: "boolean",
description: "Include the PGN tags",
default: true
},
clocks: {
type: "boolean",
description: "Include clock comments in the PGN moves",
default: true
},
evals: {
type: "boolean",
description: "Include analysis evaluation comments",
default: true
},
accuracy: {
type: "boolean",
description: "Include accuracy percentages",
default: false
},
opening: {
type: "boolean",
description: "Include opening name",
default: true
},
literate: {
type: "boolean",
description: "Include textual annotations",
default: false
}
},
required: ["gameId"]
}
},
{
name: "export_ongoing_game",
description: "Export ongoing game of a user",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "The username"
},
moves: {
type: "boolean",
description: "Include the PGN moves",
default: true
},
pgnInJson: {
type: "boolean",
description: "Include the full PGN within the JSON response",
default: false
},
tags: {
type: "boolean",
description: "Include the PGN tags",
default: true
},
clocks: {
type: "boolean",
description: "Include clock comments in the PGN moves",
default: true
},
evals: {
type: "boolean",
description: "Include analysis evaluation comments",
default: true
},
opening: {
type: "boolean",
description: "Include opening name",
default: true
}
},
required: ["username"]
}
},
{
name: "export_user_games",
description: "Export all games of a user",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "The username"
},
since: {
type: "number",
description: "Download games played since timestamp"
},
until: {
type: "number",
description: "Download games played until timestamp"
},
max: {
type: "number",
description: "Maximum number of games to download"
},
vs: {
type: "string",
description: "Only games against this opponent"
},
rated: {
type: "boolean",
description: "Only rated (true) or casual (false) games"
},
perfType: {
type: "string",
description: "Only games in these speeds or variants",
enum: ["ultraBullet", "bullet", "blitz", "rapid", "classical", "correspondence", "chess960", "crazyhouse", "antichess", "atomic", "horde", "kingOfTheHill", "racingKings", "threeCheck"]
},
color: {
type: "string",
description: "Only games played as this color",
enum: ["white", "black"]
},
analysed: {
type: "boolean",
description: "Only games with or without computer analysis"
},
moves: {
type: "boolean",
description: "Include moves",
default: true
},
tags: {
type: "boolean",
description: "Include tags",
default: true
},
clocks: {
type: "boolean",
description: "Include clock comments",
default: false
},
evals: {
type: "boolean",
description: "Include analysis",
default: false
},
accuracy: {
type: "boolean",
description: "Include accuracy",
default: false
},
opening: {
type: "boolean",
description: "Include opening",
default: false
},
ongoing: {
type: "boolean",
description: "Include ongoing games",
default: false
},
finished: {
type: "boolean",
description: "Include finished games",
default: true
},
literate: {
type: "boolean",
description: "Include textual annotations",
default: false
},
lastFen: {
type: "boolean",
description: "Include last position FEN",
default: false
},
sort: {
type: "string",
description: "Sort order of games",
enum: ["dateAsc", "dateDesc"],
default: "dateDesc"
}
},
required: ["username"]
}
},
{
name: "export_games_by_ids",
description: "Export multiple games by IDs",
inputSchema: {
type: "object",
properties: {
ids: {
type: "string",
description: "Game IDs separated by commas. Up to 300 IDs."
},
moves: {
type: "boolean",
description: "Include the PGN moves",
default: true
},
pgnInJson: {
type: "boolean",
description: "Include the full PGN within the JSON response",
default: false
},
tags: {
type: "boolean",
description: "Include the PGN tags",
default: true
},
clocks: {
type: "boolean",
description: "Include clock comments",
default: false
},
evals: {
type: "boolean",
description: "Include analysis",
default: false
},
opening: {
type: "boolean",
description: "Include opening name",
default: false
}
},
required: ["ids"]
}
},
{
name: "get_tv_channels",
description: "Get all TV channels and their current games",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_tv_game",
description: "Get current TV game in PGN format",
inputSchema: {
type: "object",
properties: {
channel: {
type: "string",
description: "Channel name like 'bot', 'blitz', etc.",
enum: [ "bot",
"blitz",
"racingKings",
"ultraBullet",
"bullet",
"classical",
"threeCheck",
"antichess",
"computer",
"horde",
"rapid",
"atomic",
"crazyhouse",
"chess960",
"kingOfTheHill",
"best"]
}
}
}
},
{
name: "get_puzzle_activity",
description: "Get your puzzle activity",
inputSchema: {
type: "object",
properties: {
max: {
type: "number",
description: "How many entries to download. Leave empty to get all activity.",
minimum: 1,
maximum: 200
}
}
}
},
{
name: "get_puzzle_dashboard",
description: "Get your puzzle dashboard",
inputSchema: {
type: "object",
properties: {
days: {
type: "number",
description: "How many days of history to return (max 30)",
minimum: 1,
maximum: 30,
default: 30
}
}
}
},
{
name: "get_puzzle_race",
description: "Get info about a puzzle race",
inputSchema: {
type: "object",
properties: {
raceId: {
type: "string",
description: "ID of the puzzle race"
}
},
required: ["raceId"]
}
},
{
name: "create_puzzle_race",
description: "Create a puzzle race",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_puzzle_storm_dashboard",
description: "Get your puzzle storm dashboard",
inputSchema: {
type: "object",
properties: {
days: {
type: "number",
description: "How many days of history to return (max 30)",
minimum: 1,
maximum: 30,
default: 30
}
}
}
},
{
name: "get_team_info",
description: "Get team information by ID",
inputSchema: {
type: "object",
properties: {
teamId: {
type: "string",
description: "The team ID"
}
},
required: ["teamId"]
}
},
{
name: "get_team_members",
description: "Get members of a team",
inputSchema: {
type: "object",
properties: {
teamId: {
type: "string",
description: "The team ID"
},
max: {
type: "number",
description: "Maximum number of members to fetch",
default: 100
}
},
required: ["teamId"]
}
},
{
name: "get_team_join_requests",
description: "Get join requests for a team",
inputSchema: {
type: "object",
properties: {
teamId: {
type: "string",
description: "The team ID"
}
},
required: ["teamId"]
}
},
{
name: "join_team",
description: "Join a team",
inputSchema: {
type: "object",
properties: {
teamId: {
type: "string",
description: "The team ID"
},
message: {
type: "string",
description: "Optional message for team leaders"
}
},
required: ["teamId"]
}
},
{
name: "leave_team",
description: "Leave a team",
inputSchema: {
type: "object",
properties: {
teamId: {
type: "string",
description: "The team ID"
}
},
required: ["teamId"]
}
},
{
name: "kick_user_from_team",
description: "Kick a user from your team",
inputSchema: {
type: "object",
properties: {
teamId: {
type: "string",
description: "The team ID"
},
userId: {
type: "string",
description: "The user ID"
}
},
required: ["teamId", "userId"]
}
},
{
name: "accept_join_request",
description: "Accept a join request for your team",
inputSchema: {
type: "object",
properties: {
teamId: {
type: "string",
description: "The team ID"
},
userId: {
type: "string",
description: "The user ID"
}
},
required: ["teamId", "userId"]
}
},
{
name: "decline_join_request",
description: "Decline a join request for your team",
inputSchema: {
type: "object",
properties: {
teamId: {
type: "string",
description: "The team ID"
},
userId: {
type: "string",
description: "The user ID"
}
},
required: ["teamId", "userId"]
}
},
{
name: "search_teams",
description: "Search for teams",
inputSchema: {
type: "object",
properties: {
text: {
type: "string",
description: "Search text"
},
page: {
type: "number",
description: "Page number (starting at 1)",
default: 1
}
},
required: ["text"]
}
},
{
name: "make_board_move",
description: "Make a move in a board game",
inputSchema: {
type: "object",
properties: {
gameId: {
type: "string",
description: "The game ID"
},
move: {
type: "string",
description: "Move in UCI format (e.g. e2e4)"
},
offeringDraw: {
type: "boolean",
description: "Whether to offer/accept a draw",
default: false
}
},
required: ["gameId", "move"]
}
},
{
name: "abort_board_game",
description: "Abort a board game",
inputSchema: {
type: "object",
properties: {
gameId: {
type: "string",
description: "The game ID"
}
},
required: ["gameId"]
}
},
{
name: "resign_board_game",
description: "Resign a board game",
inputSchema: {
type: "object",
properties: {
gameId: {
type: "string",
description: "The game ID"
}
},
required: ["gameId"]
}
},
{
name: "write_in_chat",
description: "Write in the chat of a board game",
inputSchema: {
type: "object",
properties: {
gameId: {
type: "string",
description: "The game ID"
},
room: {
type: "string",
description: "The chat room",
enum: ["player", "spectator"]
},
text: {
type: "string",
description: "The message to send"
}
},
required: ["gameId", "room", "text"]
}
},
{
name: "handle_draw_board_game",
description: "Handle draw offers for a board game",
inputSchema: {
type: "object",
properties: {
gameId: {
type: "string",
description: "The game ID"
},
accept: {
type: "boolean",
description: "Whether to accept or decline the draw offer",
default: true
}
},
required: ["gameId"]
}
},
{
name: "claim_victory",
description: "Claim victory if opponent abandoned the game",
inputSchema: {
type: "object",
properties: {
gameId: {
type: "string",
description: "The game ID"
}
},
required: ["gameId"]
}
},
{
name: "list_challenges",
description: "List incoming and outgoing challenges",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "create_challenge",
description: "Challenge another player",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player to challenge"
},
rated: {
type: "boolean",
description: "Whether the game is rated",
default: false
},
clock: {
type: "object",
description: "Clock settings",
properties: {
limit: {
type: "number",
description: "Clock initial time in minutes"
},
increment: {
type: "number",
description: "Clock increment in seconds"
}
}
},
days: {
type: "number",
description: "Days per turn for correspondence games"
},
color: {
type: "string",
description: "Color to play",
enum: ["random", "white", "black"]
},
variant: {
type: "string",
description: "Game variant",
enum: ["standard", "chess960", "crazyhouse", "antichess", "atomic", "horde", "kingOfTheHill", "racingKings", "threeCheck"],
default: "standard"
},
fen: {
type: "string",
description: "Custom initial position in FEN format"
}
},
required: ["username"]
}
},
{
name: "accept_challenge",
description: "Accept an incoming challenge",
inputSchema: {
type: "object",
properties: {
challengeId: {
type: "string",
description: "ID of the challenge to accept"
}
},
required: ["challengeId"]
}
},
{
name: "decline_challenge",
description: "Decline an incoming challenge",
inputSchema: {
type: "object",
properties: {
challengeId: {
type: "string",
description: "ID of the challenge to decline"
},
reason: {
type: "string",
description: "Reason for declining",
enum: ["generic", "later", "tooFast", "tooSlow", "timeControl", "rated", "casual", "standard", "variant", "noBot", "onlyBot"]
}
},
required: ["challengeId"]
}
},
{
name: "cancel_challenge",
description: "Cancel an outgoing challenge",
inputSchema: {
type: "object",
properties: {
challengeId: {
type: "string",
description: "ID of the challenge to cancel"
}
},
required: ["challengeId"]
}
},
{
name: "get_arena_tournaments",
description: "Get current tournaments",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "create_arena",
description: "Create a new arena tournament",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the tournament"
},
clockTime: {
type: "number",
description: "Clock initial time in minutes",
default: 3
},
clockIncrement: {
type: "number",
description: "Clock increment in seconds",
default: 2
},
minutes: {
type: "number",
description: "Tournament duration in minutes",
default: 45
},
waitMinutes: {
type: "number",
description: "Time before tournament starts, in minutes",
default: 5
},
startDate: {
type: "number",
description: "Timestamp to start the tournament at a given date"
},
variant: {
type: "string",
description: "Variant key",
enum: ["standard", "chess960", "crazyhouse", "antichess", "atomic", "horde", "kingOfTheHill", "racingKings", "threeCheck"],
default: "standard"
},
rated: {
type: "boolean",
description: "Whether the tournament is rated",
default: true
},
position: {
type: "string",
description: "Custom initial position in FEN format"
},
berserkable: {
type: "boolean",
description: "Whether players can use berserk",
default: true
},
streakable: {
type: "boolean",
description: "Whether players can get streaks",
default: true
},
hasChat: {
type: "boolean",
description: "Whether players can discuss in a chat",
default: true
},
description: {
type: "string",
description: "Tournament description (HTML)"
},
conditions: {
type: "object",
description: "Restrict participation",
properties: {
nbRatedGame: {
type: "number",
description: "Minimum number of rated games required"
},
minRating: {
type: "number",
description: "Minimum rating required"
},
maxRating: {
type: "number",
description: "Maximum rating allowed"
},
teamMember: {
type: "string",
description: "Team ID required to join"
},
allowList: {
type: "string",
description: "List of usernames allowed to join"
}
}
}
},
required: ["name"]
}
},
{
name: "get_arena_info",
description: "Get info about an arena tournament",
inputSchema: {
type: "object",
properties: {
tournamentId: {
type: "string",
description: "Tournament ID"
}
},
required: ["tournamentId"]
}
},
{
name: "get_arena_games",
description: "Get games of an arena tournament",
inputSchema: {
type: "object",
properties: {
tournamentId: {
type: "string",
description: "Tournament ID"
}
},
required: ["tournamentId"]
}
},
{
name: "get_arena_results",
description: "Get results of an arena tournament",
inputSchema: {
type: "object",
properties: {
tournamentId: {
type: "string",
description: "Tournament ID"
}
},
required: ["tournamentId"]
}
},
{
name: "join_arena",
description: "Join an arena tournament",
inputSchema: {
type: "object",
properties: {
tournamentId: {
type: "string",
description: "Tournament ID"
}
},
required: ["tournamentId"]
}
},
{
name: "withdraw_from_arena",
description: "Withdraw from an arena tournament",
inputSchema: {
type: "object",
properties: {
tournamentId: {
type: "string",
description: "Tournament ID"
}
},
required: ["tournamentId"]
}
},
{
name: "get_team_battle_results",
description: "Get results of a team battle tournament",
inputSchema: {
type: "object",
properties: {
tournamentId: {
type: "string",
description: "Tournament ID"
}
},
required: ["tournamentId"]
}
},
{
name: "create_swiss",
description: "Create a new Swiss tournament",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the tournament"
},
teamId: {
type: "string",
description: "ID of the team hosting the tournament"
},
clock: {
type: "object",
description: "Clock settings",
properties: {
limit: {
type: "number",
description: "Clock initial time in seconds"
},
increment: {
type: "number",
description: "Clock increment in seconds"
}
},
required: ["limit", "increment"]
},
nbRounds: {
type: "number",
description: "Number of rounds to play",
default: 7
},
variant: {
type: "string",
description: "Variant key",
enum: ["standard", "chess960", "crazyhouse", "antichess", "atomic", "horde", "kingOfTheHill", "racingKings", "threeCheck"],
default: "standard"
},
rated: {
type: "boolean",
description: "Whether the tournament is rated",
default: true
},
description: {
type: "string",
description: "Tournament description (HTML)"
},
roundInterval: {
type: "number",
description: "Interval between rounds in seconds",
default: 300
}
},
required: ["name", "teamId", "clock"]
}
},
{
name: "get_swiss_info",
description: "Get info about a Swiss tournament",
inputSchema: {
type: "object",
properties: {
swissId: {
type: "string",
description: "Swiss tournament ID"
}
},
required: ["swissId"]
}
},
{
name: "get_swiss_games",
description: "Get games of a Swiss tournament",
inputSchema: {
type: "object",
properties: {
swissId: {
type: "string",
description: "Swiss tournament ID"
}
},
required: ["swissId"]
}
},
{
name: "get_swiss_results",
description: "Get results of a Swiss tournament",
inputSchema: {
type: "object",
properties: {
swissId: {
type: "string",
description: "Swiss tournament ID"
}
},
required: ["swissId"]
}
},
{
name: "join_swiss",
description: "Join a Swiss tournament",
inputSchema: {
type: "object",
properties: {
swissId: {
type: "string",
description: "Swiss tournament ID"
}
},
required: ["swissId"]
}
},
{
name: "withdraw_from_swiss",
description: "Withdraw from a Swiss tournament",
inputSchema: {
type: "object",
properties: {
swissId: {
type: "string",
description: "Swiss tournament ID"
}
},
required: ["swissId"]
}
},
{
name: "get_current_simuls",
description: "Get recently started simuls",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "create_simul",
description: "Create a new simul",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the simul"
},
variant: {
type: "string",
description: "Variant key",
enum: ["standard", "chess960", "crazyhouse", "antichess", "atomic", "horde", "kingOfTheHill", "racingKings", "threeCheck"],
default: "standard"
},
clockTime: {
type: "number",
description: "Clock initial time in minutes",
default: 5
},
clockIncrement: {
type: "number",
description: "Clock increment in seconds",
default: 3
},
minRating: {
type: "number",
description: "Minimum rating to join"
},
maxRating: {
type: "number",
description: "Maximum rating to join"
},
color: {
type: "string",
description: "Color the host will play",
enum: ["white", "black"],
default: "white"
},
text: {
type: "string",
description: "Description of the simul"
}
},
required: ["name"]
}
},
{
name: "join_simul",
description: "Join a simul",
inputSchema: {
type: "object",
properties: {
simulId: {
type: "string",
description: "ID of the simul"
}
},
required: ["simulId"]
}
},
{
name: "withdraw_from_simul",
description: "Withdraw from a simul",
inputSchema: {
type: "object",
properties: {
simulId: {
type: "string",
description: "ID of the simul"
}
},
required: ["simulId"]
}
},
{
name: "export_study_chapter",
description: "Export one study chapter in PGN format",
inputSchema: {
type: "object",
properties: {
studyId: {
type: "string",
description: "Study ID"
},
chapterId: {
type: "string",
description: "Chapter ID"
}
},
required: ["studyId", "chapterId"]
}
},
{
name: "export_all_study_chapters",
description: "Export all chapters of a study in PGN format",
inputSchema: {
type: "object",
properties: {
studyId: {
type: "string",
description: "Study ID"
}
},
required: ["studyId"]
}
},
{
name: "get_user_studies",
description: "Get studies created by a user",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the player"
}
},
required: ["username"]
}
},
{
name: "send_message",
description: "Send a private message to another player",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "Username of the recipient"
},
text: {
type: "string",
description: "Message text"
}
},
required: ["username", "text"]
}
},
{
name: "get_thread",
description: "Get a message thread",
inputSchema: {
type: "object",
properties: {
userId: {
type: "string",
description: "User ID of the other person"
}
},
required: ["userId"]
}
},
{
name: "get_official_broadcasts",
description: "Get official broadcasts (TV shows)",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_broadcast",
description: "Get a broadcast by its ID",
inputSchema: {
type: "object",
properties: {
broadcastId: {
type: "string",
description: "ID of the broadcast"
}
},
required: ["broadcastId"]
}
},
{
name: "get_broadcast_round",
description: "Get one round of a broadcast",
inputSchema: {
type: "object",
properties: {
broadcastId: {
type: "string",
description: "ID of the broadcast"
},
roundId: {
type: "string",
description: "ID of the round"
}
},
required: ["broadcastId", "roundId"]
}
},
{
name: "push_broadcast_round_pgn",
description: "Push PGN to a broadcast round",
inputSchema: {
type: "object",
properties: {
broadcastId: {
type: "string",
description: "ID of the broadcast"
},
roundId: {
type: "string",
description: "ID of the round"
},
pgn: {
type: "string",
description: "PGN games to push"
}
},
required: ["broadcastId", "roundId", "pgn"]
}
},
{
name: "get_cloud_eval",
description: "Get cloud evaluation for a position",
inputSchema: {
type: "object",
properties: {
fen: {
type: "string",
description: "FEN of the position to analyze"
},
multiPv: {
type: "number",
description: "Number of principal variations (1-5)",
minimum: 1,
maximum: 5,
default: 1
}
},
required: ["fen"]
}
},
{
name: "get_fide_player",
description: "Get FIDE player information",
inputSchema: {
type: "object",
properties: {
playerId: { // Changed from username
type: "string",
description: "FIDE player ID"
}
},
required: ["playerId"]
}
},
{
name: "search_fide_players",
description: "Search FIDE players by name",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the player to search"
}
},
required: ["name"]
}
},
{
name: "get_ongoing_games",
description: "Get your ongoing games (real-time and correspondence)",
inputSchema: {
type: "object",
properties: {
nb: {
type: "integer",
description: "Max number of games to fetch (1-50)",
minimum: 1,
maximum: 50,
default: 9
}
}
}
}
]
};
});
/**
* Handler for tool calls
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "set_token": {
const token = String(request.params.arguments?.token);
if (!token) {
throw new Error("Token is required");
}
LICHESS_TOKEN = token;
return {
content: [{
type: "text",
text: "Lichess API token has been set"
}]
};
}
case "get_my_profile": {
const response = await lichessRequest('/account');
const profile = await response.json() as Profile;
return {
content: [{
type: "text",
text: JSON.stringify(profile, null, 2)
}]
};
}
case "get_user_profile": {
const username = String(request.params.arguments?.username);
const trophies = Boolean(request.params.arguments?.trophies);
const response = await lichessRequest(`/user/${username}${trophies ? '?trophies=true' : ''}`);
const profile = await response.json() as Profile;
return {
content: [{
type: "text",
text: JSON.stringify(profile, null, 2)
}]
};
}
case "get_my_email": {
const response = await lichessRequest('/account/email');
const emailData = await response.json() as EmailResponse;
return {
content: [{
type: "text",
text: `Your email address is: ${emailData.email}`
}]
};
}
case "get_kid_mode": {
const response = await lichessRequest('/account/kid');
const kidData = await response.json() as KidModeResponse;
return {
content: [{
type: "text",
text: `Kid mode is ${kidData.kid ? 'enabled' : 'disabled'}`
}]
};
}
case "set_kid_mode": {
const value = Boolean(request.params.arguments?.value);
await lichessRequest(`/account/kid?v=${value}`, {
method: 'POST'
});
return {
content: [{
type: "text",
text: `Kid mode has been ${value ? 'enabled' : 'disabled'}`
}]
};
}
case "create_challenge": {
const username = String(request.params.arguments?.username);
const params = new URLSearchParams();
// Add basic parameters
if (request.params.arguments?.rated !== undefined) {
params.append('rated', String(request.params.arguments.rated));
}
if (request.params.arguments?.color) {
params.append('color', String(request.params.arguments.color));
}
if (request.params.arguments?.variant) {
params.append('variant', String(request.params.arguments.variant));
}
// Add clock settings if provided
if (request.params.arguments?.clock) {
const clock = request.params.arguments.clock as { limit?: number; increment?: number };
if (clock.limit !== undefined) {
params.append('clock.limit', String(clock.limit * 60)); // Convert minutes to seconds
}
if (clock.increment !== undefined) {
params.append('clock.increment', String(clock.increment));
}
}
// Add days for correspondence games
if (request.params.arguments?.days) {
params.append('days', String(request.params.arguments.days));
}
// Add custom initial position if provided
if (request.params.arguments?.fen) {
params.append('fen', String(request.params.arguments.fen));
}
const response = await lichessRequest(`/challenge/${username}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
const challenge = await response.json() as ChallengeResponse;
return {
content: [{
type: "text",
text: `Challenge created: ${challenge.challenge.url}`
}]
};
}
case "make_move": {
const gameId = String(request.params.arguments?.gameId);
const move = String(request.params.arguments?.move);
const offeringDraw = Boolean(request.params.arguments?.offeringDraw);
const url = new URL(`/board/game/${gameId}/move/${move}`, LICHESS_API_URL);
if (offeringDraw) {
url.searchParams.append('offeringDraw', 'true');
}
await lichessRequest(url.pathname + url.search, {
method: 'POST'
});
return {
content: [{
type: "text",
text: `Move ${move} made in game ${gameId}${offeringDraw ? ' with draw offer' : ''}`
}]
};
}
case "get_preferences": {
const response = await lichessRequest('/account/preferences');
const preferences = await response.json() as Preferences;
return {
content: [{
type: "text",
text: JSON.stringify(preferences, null, 2)
}]
};
}
case "get_timeline": {
const since = request.params.arguments?.since;
const nb = request.params.arguments?.nb || 15;
const queryParams = new URLSearchParams();
if (since) queryParams.append('since', String(since));
if (nb) queryParams.append('nb', String(nb));
const response = await lichessRequest(`/timeline?${queryParams.toString()}`);
const timeline = await response.json() as TimelineEntry[];
return {
content: [{
type: "text",
text: JSON.stringify(timeline, null, 2)
}]
};
}
case "test_tokens": {
const tokens = String(request.params.arguments?.tokens);
if (!tokens) {
throw new Error("Tokens parameter is required");
}
const tokenCount = tokens.split(',').length;
if (tokenCount > 1000) {
throw new Error("Maximum of 1000 tokens allowed");
}
// Don't use lichessRequest here since we don't want to add the auth header
const response = await fetch(`${LICHESS_API_URL}/token/test`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: tokens
});
if (!response.ok) {
throw new Error(`Lichess API error: ${response.statusText}`);
}
const results = await response.json() as TokenTestResult;
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2)
}]
};
}
case "revoke_token": {
if (!LICHESS_TOKEN) {
throw new Error('No token set to revoke. Please set a token first using set_token.');
}
try {
const response = await fetch(`${LICHESS_API_URL}/token`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${LICHESS_TOKEN}`
}
});
if (!response.ok) {
throw new Error(`Failed to revoke token: ${response.statusText}`);
}
// Successfully revoked - clear the token
LICHESS_TOKEN = undefined;
return {
content: [{
type: "text",
text: "Access token has been successfully revoked and cleared"
}]
};
} catch (error: any) {
// If there was an error, don't clear the token as it may not have been revoked
throw new Error(`Failed to revoke token: ${error.message || 'Unknown error'}`);
}
}
case "upgrade_to_bot": {
await lichessRequest('/bot/account/upgrade', {
method: 'POST'
});
return {
content: [{
type: "text",
text: "Account has been successfully upgraded to a Bot account. The account can now only play as a Bot."
}]
};
}
case "add_user_note": {
const username = String(request.params.arguments?.username);
if (!username) {
throw new Error('Username parameter is required');
}
const text = String(request.params.arguments?.text);
if (!text) {
throw new Error('Text parameter is required');
}
try {
const response = await lichessRequest(`/user/${username}/note`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({ text }).toString()
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to add note: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Note successfully added for user ${username}`
}]
};
} catch (error: any) {
throw new Error(`Failed to add note: ${error.message || 'Unknown error'}`);
}
}
case "send_message": {
const username = String(request.params.arguments?.username);
if (!username) {
throw new Error('Username parameter is required');
}
const text = String(request.params.arguments?.text);
if (!text) {
throw new Error('Text parameter is required');
}
try {
const response = await lichessRequest(`/inbox/${username}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({ text }).toString()
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to send message: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Message successfully sent to ${username}`
}]
};
} catch (error: any) {
throw new Error(`Failed to send message: ${error.message || 'Unknown error'}`);
}
}
case "get_following": {
try {
const response = await lichessRequest('/rel/following');
if (!response.ok) {
throw new Error(`Failed to get following list: ${response.statusText}`);
}
// Read the response as text
const text = await response.text();
// Split by newlines and parse each line as JSON
const following = text
.split('\n')
.filter(line => line.trim()) // Remove empty lines
.map(line => JSON.parse(line));
return {
content: [{
type: "text",
text: JSON.stringify(following, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get following list: ${error.message || 'Unknown error'}`);
}
}
case "follow_user": {
const username = String(request.params.arguments?.username);
if (!username) {
throw new Error('Username parameter is required');
}
try {
const response = await lichessRequest(`/rel/follow/${username}`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
if (response.status === 400) {
throw new Error(`Cannot follow ${username}: invalid request (you may be trying to follow yourself)`);
}
throw new Error(`Failed to follow user: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully following ${username}`
}]
};
} catch (error: any) {
throw new Error(`Failed to follow user: ${error.message || 'Unknown error'}`);
}
}
case "unfollow_user": {
const username = String(request.params.arguments?.username);
if (!username) {
throw new Error('Username parameter is required');
}
try {
const response = await lichessRequest(`/rel/unfollow/${username}`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to unfollow user: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully unfollowed ${username}`
}]
};
} catch (error: any) {
throw new Error(`Failed to unfollow user: ${error.message || 'Unknown error'}`);
}
}
case "block_user": {
const username = String(request.params.arguments?.username);
if (!username) {
throw new Error('Username parameter is required');
}
try {
const response = await lichessRequest(`/rel/block/${username}`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to block user: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully blocked ${username}`
}]
};
} catch (error: any) {
throw new Error(`Failed to block user: ${error.message || 'Unknown error'}`);
}
}
case "get_users_status": {
const ids = String(request.params.arguments?.ids);
if (!ids) {
throw new Error('IDs parameter is required');
}
const idList = ids.split(',');
if (idList.length > 100) {
throw new Error('Maximum of 100 user IDs allowed');
}
const withSignal = Boolean(request.params.arguments?.withSignal);
const withGameIds = Boolean(request.params.arguments?.withGameIds);
const withGameMetas = Boolean(request.params.arguments?.withGameMetas);
try {
const params = new URLSearchParams();
if (withSignal) params.append('withSignal', 'true');
if (withGameIds) params.append('withGameIds', 'true');
if (withGameMetas) params.append('withGameMetas', 'true');
const response = await lichessRequest(`/users/status?ids=${ids}&${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to get user statuses: ${response.statusText}`);
}
const status = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(status, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get user statuses: ${error.message || 'Unknown error'}`);
}
}
case "get_all_top_10": {
const response = await lichessRequest('/player');
const top10s = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(top10s, null, 2)
}]
};
}
case "get_leaderboard": {
const perfType = String(request.params.arguments?.perfType);
if (!perfType) {
throw new Error('perfType parameter is required');
}
const validPerfTypes = [
"ultraBullet", "bullet", "blitz", "rapid", "classical",
"chess960", "crazyhouse", "antichess", "atomic", "horde",
"kingOfTheHill", "racingKings", "threeCheck"
];
if (!validPerfTypes.includes(perfType)) {
throw new Error(`Invalid perfType. Must be one of: ${validPerfTypes.join(', ')}`);
}
const nb = Number(request.params.arguments?.nb) || 100;
if (nb < 1 || nb > 200) {
throw new Error('nb parameter must be between 1 and 200');
}
const response = await lichessRequest(`/player/top/${nb}/${perfType}`);
const leaderboard = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(leaderboard, null, 2)
}]
};
}
case "get_user_public_data": {
const username = String(request.params.arguments?.username);
if (!username) {
throw new Error('Username parameter is required');
}
if (username.trim() === '') {
throw new Error('Username cannot be empty');
}
const withTrophies = Boolean(request.params.arguments?.withTrophies);
const params = new URLSearchParams();
if (withTrophies) {
params.append('trophies', 'true');
}
try {
const response = await lichessRequest(`/user/${username}?${params.toString()}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to get user data: ${response.statusText}`);
}
const userData = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(userData, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get user data: ${error.message || 'Unknown error'}`);
}
}
case "get_rating_history": {
const username = String(request.params.arguments?.username);
if (!username) {
throw new Error('Username parameter is required');
}
if (username.trim() === '') {
throw new Error('Username cannot be empty');
}
try {
const response = await lichessRequest(`/user/${username}/rating-history`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to get rating history: ${response.statusText}`);
}
const ratingHistory = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(ratingHistory, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get rating history: ${error.message || 'Unknown error'}`);
}
}
case "get_user_performance": {
const username = String(request.params.arguments?.username);
const perf = String(request.params.arguments?.perf);
const response = await lichessRequest(`/user/${username}/perf/${perf}`);
const perfStats = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(perfStats, null, 2)
}]
};
}
case "get_user_activity": {
const username = String(request.params.arguments?.username);
if (!username) {
throw new Error('Username parameter is required');
}
if (username.trim() === '') {
throw new Error('Username cannot be empty');
}
try {
const response = await lichessRequest(`/user/${username}/activity`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to get user activity: ${response.statusText}`);
}
const activity = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(activity, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get user activity: ${error.message || 'Unknown error'}`);
}
}
case "get_users_by_id": {
const ids = String(request.params.arguments?.ids);
if (!ids) {
throw new Error('IDs parameter is required');
}
if (ids.trim() === '') {
throw new Error('IDs cannot be empty');
}
const idList = ids.split(',');
if (idList.length > 300) {
throw new Error('Maximum of 300 user IDs allowed');
}
try {
const response = await lichessRequest('/users', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: ids
});
if (!response.ok) {
throw new Error(`Failed to get users: ${response.statusText}`);
}
const users = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(users, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get users: ${error.message || 'Unknown error'}`);
}
}
case "unblock_user": {
const username = String(request.params.arguments?.username);
if (!username) {
throw new Error('Username parameter is required');
}
if (username.trim() === '') {
throw new Error('Username cannot be empty');
}
try {
const response = await lichessRequest(`/rel/unblock/${username}`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to unblock user: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully unblocked ${username}`
}]
};
} catch (error: any) {
throw new Error(`Failed to unblock user: ${error.message || 'Unknown error'}`);
}
}
case "export_game": {
const gameId = String(request.params.arguments?.gameId);
// Validate gameId
if (!gameId || gameId.length !== 8) {
throw new Error('Game ID must be exactly 8 characters long');
}
const params = new URLSearchParams();
// Add optional parameters with proper validation
const booleanParams = ['moves', 'pgnInJson', 'tags', 'clocks', 'evals', 'accuracy', 'opening', 'literate'];
for (const param of booleanParams) {
if (request.params.arguments?.[param] !== undefined) {
params.append(param, String(request.params.arguments[param]));
}
}
try {
const response = await lichessRequest(`/game/export/${gameId}?${params.toString()}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Game ${gameId} not found`);
}
throw new Error(`Failed to export game: ${response.statusText}`);
}
// Check if response is PGN or JSON
const contentType = response.headers.get('content-type');
let content;
if (contentType?.includes('application/x-chess-pgn')) {
content = await response.text();
} else {
content = await response.json();
}
return {
content: [{
type: "text",
text: typeof content === 'string' ? content : JSON.stringify(content, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to export game: ${error.message || 'Unknown error'}`);
}
}
case "export_ongoing_game": {
const username = String(request.params.arguments?.username);
// Validate username
if (!username) {
throw new Error('Username parameter is required');
}
if (username.trim() === '') {
throw new Error('Username cannot be empty');
}
const params = new URLSearchParams();
// Add optional parameters with proper validation
const booleanParams = ['moves', 'pgnInJson', 'tags', 'clocks', 'evals', 'opening'];
for (const param of booleanParams) {
if (request.params.arguments?.[param] !== undefined) {
params.append(param, String(request.params.arguments[param]));
}
}
try {
const response = await lichessRequest(`/user/${username}/current-game?${params.toString()}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found or has no ongoing game`);
}
throw new Error(`Failed to export ongoing game: ${response.statusText}`);
}
// Check if response is PGN or JSON
const contentType = response.headers.get('content-type');
let content;
if (contentType?.includes('application/x-chess-pgn')) {
content = await response.text();
} else {
content = await response.json();
}
return {
content: [{
type: "text",
text: typeof content === 'string' ? content : JSON.stringify(content, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to export ongoing game: ${error.message || 'Unknown error'}`);
}
}
case "export_user_games": {
const username = String(request.params.arguments?.username);
// Validate username
if (!username) {
throw new Error('Username parameter is required');
}
if (username.trim() === '') {
throw new Error('Username cannot be empty');
}
const params = new URLSearchParams();
// Add timestamp parameters with validation
if (request.params.arguments?.since !== undefined) {
const since = Number(request.params.arguments.since);
if (since < 1356998400070) {
throw new Error('Since timestamp must be after January 1, 2013');
}
params.append('since', String(since));
}
if (request.params.arguments?.until !== undefined) {
const until = Number(request.params.arguments.until);
if (until < 1356998400070) {
throw new Error('Until timestamp must be after January 1, 2013');
}
params.append('until', String(until));
}
if (request.params.arguments?.max !== undefined) {
const max = Number(request.params.arguments.max);
if (max < 1) {
throw new Error('Max number of games must be at least 1');
}
params.append('max', String(max));
}
// Add string parameters
const stringParams = ['vs', 'perfType', 'color', 'players', 'sort'] as const;
for (const param of stringParams) {
const value = String(request.params.arguments?.[param] || '');
if (value) {
// Validate enum values where applicable
if (param === 'color' && !['white', 'black'].includes(value)) {
throw new Error('Color must be either "white" or "black"');
}
if (param === 'sort' && !['dateAsc', 'dateDesc'].includes(value)) {
throw new Error('Sort must be either "dateAsc" or "dateDesc"');
}
if (param === 'perfType') {
const validPerfTypes = ['ultraBullet', 'bullet', 'blitz', 'rapid', 'classical', 'correspondence',
'chess960', 'crazyhouse', 'antichess', 'atomic', 'horde', 'kingOfTheHill',
'racingKings', 'threeCheck'];
if (!validPerfTypes.includes(value)) {
throw new Error('Invalid perfType value');
}
}
params.append(param, value);
}
}
// Add boolean parameters
const booleanParams = ['rated', 'analysed', 'moves', 'tags', 'clocks', 'evals',
'accuracy', 'opening', 'ongoing', 'finished', 'literate', 'lastFen'];
for (const param of booleanParams) {
if (request.params.arguments?.[param] !== undefined) {
params.append(param, String(request.params.arguments[param]));
}
}
try {
const response = await lichessRequest(`/games/user/${username}?${params.toString()}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to export games: ${response.statusText}`);
}
// Check if response is PGN or NDJSON
const contentType = response.headers.get('content-type');
let content;
if (contentType?.includes('application/x-chess-pgn')) {
content = await response.text();
} else if (contentType?.includes('application/x-ndjson')) {
// For NDJSON, we need to handle the streaming format
const text = await response.text();
content = text.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
} else {
throw new Error('Unexpected response format');
}
return {
content: [{
type: "text",
text: typeof content === 'string' ? content : JSON.stringify(content, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to export games: ${error.message || 'Unknown error'}`);
}
}
case "export_games_by_ids": {
const ids = String(request.params.arguments?.ids);
// Validate IDs
if (!ids) {
throw new Error('Game IDs parameter is required');
}
const idList = ids.split(',');
if (idList.length > 300) {
throw new Error('Maximum of 300 game IDs allowed');
}
// Add optional parameters
const params = new URLSearchParams();
const booleanParams = ['moves', 'pgnInJson', 'tags', 'clocks', 'evals', 'opening'];
for (const param of booleanParams) {
if (request.params.arguments?.[param] !== undefined) {
params.append(param, String(request.params.arguments[param]));
}
}
try {
const response = await lichessRequest('/games/export/_ids', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: ids
});
if (!response.ok) {
throw new Error(`Failed to export games: ${response.statusText}`);
}
// Check content type to determine format
const contentType = response.headers.get('content-type');
let content;
if (contentType?.includes('application/x-chess-pgn')) {
content = await response.text();
} else if (contentType?.includes('application/x-ndjson')) {
// For NDJSON, handle the streaming format
const text = await response.text();
content = text.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
} else {
// Default to JSON
content = await response.json();
}
return {
content: [{
type: "text",
text: typeof content === 'string' ? content : JSON.stringify(content, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to export games: ${error.message || 'Unknown error'}`);
}
}
case "get_tv_channels": {
try {
const response = await lichessRequest('/tv/channels');
if (!response.ok) {
throw new Error(`Failed to get TV channels: ${response.statusText}`);
}
const channels = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(channels, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get TV channels: ${error.message || 'Unknown error'}`);
}
}
case "get_tv_game": {
const channel = String(request.params.arguments?.channel || '');
// Validate channel if provided
const validChannels = [
"bot",
"blitz",
"racingKings",
"ultraBullet",
"bullet",
"classical",
"threeCheck",
"antichess",
"computer",
"horde",
"rapid",
"atomic",
"crazyhouse",
"chess960",
"kingOfTheHill",
"best"
];
if (channel && !validChannels.includes(channel)) {
throw new Error(`Invalid channel. Must be one of: ${validChannels.join(', ')}`);
}
try {
const path = channel ? `/tv/${channel}` : '/tv';
const response = await lichessRequest(path);
if (!response.ok) {
throw new Error(`Failed to get TV game: ${response.statusText}`);
}
// Get the content type to determine the format
const contentType = response.headers.get('content-type');
let content;
if (contentType?.includes('application/x-chess-pgn')) {
// Handle PGN format
content = await response.text();
} else {
// Handle JSON format if available
content = await response.json();
}
return {
content: [{
type: "text",
text: typeof content === 'string' ? content : JSON.stringify(content, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get TV game: ${error.message || 'Unknown error'}`);
}
}
case "get_puzzle_activity": {
const max = request.params.arguments?.max;
// Validate max parameter if provided
if (max !== undefined) {
const maxNum = Number(max);
if (isNaN(maxNum)) {
throw new Error('max parameter must be a number');
}
if (maxNum < 1 || maxNum > 200) {
throw new Error('max parameter must be between 1 and 200');
}
}
const params = new URLSearchParams();
if (max !== undefined) {
params.append('max', String(max));
}
try {
const response = await lichessRequest(`/puzzle/activity?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to get puzzle activity: ${response.statusText}`);
}
const activity = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(activity, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get puzzle activity: ${error.message || 'Unknown error'}`);
}
}
case "get_puzzle_dashboard": {
const days = Number(request.params.arguments?.days) || 30;
// Validate days parameter
if (isNaN(days)) {
throw new Error('days parameter must be a number');
}
if (days < 1) {
throw new Error('days parameter must be at least 1');
}
if (days > 30) {
throw new Error('days parameter must not exceed 30');
}
try {
const response = await lichessRequest(`/puzzle/dashboard/${days}`);
if (!response.ok) {
throw new Error(`Failed to get puzzle dashboard: ${response.statusText}`);
}
const dashboard = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(dashboard, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get puzzle dashboard: ${error.message || 'Unknown error'}`);
}
}
case "get_puzzle_race": {
const raceId = String(request.params.arguments?.raceId);
// Validate raceId parameter
if (!raceId) {
throw new Error('raceId parameter is required');
}
if (raceId.trim() === '') {
throw new Error('raceId cannot be empty');
}
try {
const response = await lichessRequest(`/racer/${raceId}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Puzzle race ${raceId} not found`);
}
throw new Error(`Failed to get puzzle race: ${response.statusText}`);
}
const race = await response.json() as { id: string; url: string };
// Validate response format
if (!race.id || !race.url) {
throw new Error('Invalid response format from Lichess API');
}
return {
content: [{
type: "text",
text: JSON.stringify(race, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get puzzle race: ${error.message || 'Unknown error'}`);
}
}
case "create_puzzle_race": {
try {
const response = await lichessRequest('/racer', {
method: 'POST'
});
if (!response.ok) {
throw new Error(`Failed to create puzzle race: ${response.statusText}`);
}
const race = await response.json() as { id: string; url: string };
// Validate response format
if (!race.id || !race.url) {
throw new Error('Invalid response format from Lichess API');
}
return {
content: [{
type: "text",
text: JSON.stringify(race, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to create puzzle race: ${error.message || 'Unknown error'}`);
}
}
case "get_puzzle_storm_dashboard": {
const days = Number(request.params.arguments?.days) || 30;
// Validate days parameter
if (isNaN(days)) {
throw new Error('days parameter must be a number');
}
if (days < 1) {
throw new Error('days parameter must be at least 1');
}
if (days > 30) {
throw new Error('days parameter must not exceed 30');
}
try {
const response = await lichessRequest(`/storm/dashboard/${days}`);
if (!response.ok) {
throw new Error(`Failed to get puzzle storm dashboard: ${response.statusText}`);
}
const dashboard = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(dashboard, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get puzzle storm dashboard: ${error.message || 'Unknown error'}`);
}
}
case "get_team_info": {
const teamId = String(request.params.arguments?.teamId);
// Validate teamId parameter
if (!teamId) {
throw new Error('teamId parameter is required');
}
if (teamId.trim() === '') {
throw new Error('teamId cannot be empty');
}
try {
const response = await lichessRequest(`/team/${teamId}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Team ${teamId} not found`);
}
throw new Error(`Failed to get team info: ${response.statusText}`);
}
const team = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(team, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get team info: ${error.message || 'Unknown error'}`);
}
}
case "get_team_members": {
const teamId = String(request.params.arguments?.teamId);
// Validate teamId parameter
if (!teamId) {
throw new Error('teamId parameter is required');
}
if (teamId.trim() === '') {
throw new Error('teamId cannot be empty');
}
const max = Number(request.params.arguments?.max) || 100;
// Validate max parameter
if (isNaN(max)) {
throw new Error('max parameter must be a number');
}
if (max < 1) {
throw new Error('max parameter must be at least 1');
}
const params = new URLSearchParams();
params.append('max', String(max));
try {
const response = await lichessRequest(`/team/${teamId}/users?${params.toString()}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Team ${teamId} not found`);
}
throw new Error(`Failed to get team members: ${response.statusText}`);
}
const members = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(members, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get team members: ${error.message || 'Unknown error'}`);
}
}
case "get_team_join_requests": {
const teamId = String(request.params.arguments?.teamId);
// Validate teamId parameter
if (!teamId) {
throw new Error('teamId parameter is required');
}
if (teamId.trim() === '') {
throw new Error('teamId cannot be empty');
}
try {
const response = await lichessRequest(`/team/${teamId}/requests`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Team ${teamId} not found`);
}
throw new Error(`Failed to get team join requests: ${response.statusText}`);
}
const requests = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(requests, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get team join requests: ${error.message || 'Unknown error'}`);
}
}
case "join_team": {
const teamId = String(request.params.arguments?.teamId);
// Validate teamId parameter
if (!teamId) {
throw new Error('teamId parameter is required');
}
if (teamId.trim() === '') {
throw new Error('teamId cannot be empty');
}
// Get optional message parameter
const message = String(request.params.arguments?.message || '');
const params = new URLSearchParams();
if (message.trim()) {
params.append('message', message);
}
try {
const response = await lichessRequest(`/team/${teamId}/join`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Team ${teamId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to join this team');
}
if (response.status === 409) {
throw new Error('You are already a member of this team');
}
throw new Error(`Failed to join team: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully joined team ${teamId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to join team: ${error.message || 'Unknown error'}`);
}
}
case "leave_team": {
const teamId = String(request.params.arguments?.teamId);
// Validate teamId parameter
if (!teamId) {
throw new Error('teamId parameter is required');
}
if (teamId.trim() === '') {
throw new Error('teamId cannot be empty');
}
try {
const response = await lichessRequest(`/team/${teamId}/quit`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Team ${teamId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to leave this team');
}
if (response.status === 409) {
throw new Error('You are not a member of this team');
}
throw new Error(`Failed to leave team: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully left team ${teamId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to leave team: ${error.message || 'Unknown error'}`);
}
}
case "kick_user_from_team": {
const teamId = String(request.params.arguments?.teamId);
const userId = String(request.params.arguments?.userId);
// Validate teamId parameter
if (!teamId) {
throw new Error('teamId parameter is required');
}
if (teamId.trim() === '') {
throw new Error('teamId cannot be empty');
}
// Validate userId parameter
if (!userId) {
throw new Error('userId parameter is required');
}
if (userId.trim() === '') {
throw new Error('userId cannot be empty');
}
try {
const response = await lichessRequest(`/team/${teamId}/kick/${userId}`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Team ${teamId} or user ${userId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to kick users from this team');
}
if (response.status === 409) {
throw new Error('User is not a member of this team');
}
throw new Error(`Failed to kick user from team: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully kicked user ${userId} from team ${teamId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to kick user from team: ${error.message || 'Unknown error'}`);
}
}
case "accept_join_request": {
const teamId = String(request.params.arguments?.teamId);
const userId = String(request.params.arguments?.userId);
// Validate teamId parameter
if (!teamId) {
throw new Error('teamId parameter is required');
}
if (teamId.trim() === '') {
throw new Error('teamId cannot be empty');
}
// Validate userId parameter
if (!userId) {
throw new Error('userId parameter is required');
}
if (userId.trim() === '') {
throw new Error('userId cannot be empty');
}
try {
const response = await lichessRequest(`/team/${teamId}/request/${userId}/accept`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Team ${teamId} or join request from user ${userId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to accept join requests for this team');
}
throw new Error(`Failed to accept join request: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully accepted join request from user ${userId} to team ${teamId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to accept join request: ${error.message || 'Unknown error'}`);
}
}
case "decline_join_request": {
const teamId = String(request.params.arguments?.teamId);
const userId = String(request.params.arguments?.userId);
// Validate teamId parameter
if (!teamId) {
throw new Error('teamId parameter is required');
}
if (teamId.trim() === '') {
throw new Error('teamId cannot be empty');
}
// Validate userId parameter
if (!userId) {
throw new Error('userId parameter is required');
}
if (userId.trim() === '') {
throw new Error('userId cannot be empty');
}
try {
const response = await lichessRequest(`/team/${teamId}/request/${userId}/decline`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Team ${teamId} or join request from user ${userId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to decline join requests for this team');
}
throw new Error(`Failed to decline join request: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully declined join request from user ${userId} to team ${teamId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to decline join request: ${error.message || 'Unknown error'}`);
}
}
case "search_teams": {
const text = String(request.params.arguments?.text);
const page = Number(request.params.arguments?.page) || 1;
// Validate text parameter
if (!text) {
throw new Error('text parameter is required');
}
if (text.trim() === '') {
throw new Error('text cannot be empty');
}
// Validate page parameter
if (isNaN(page)) {
throw new Error('page parameter must be a number');
}
if (page < 1) {
throw new Error('page parameter must be at least 1');
}
try {
const params = new URLSearchParams({
text,
page: String(page)
});
const response = await lichessRequest(`/team/search?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to search teams: ${response.statusText}`);
}
// The API returns a TeamPaginatorJson object
const teams = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(teams, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to search teams: ${error.message || 'Unknown error'}`);
}
}
case "make_board_move": {
const gameId = String(request.params.arguments?.gameId);
const move = String(request.params.arguments?.move);
const offeringDraw = Boolean(request.params.arguments?.offeringDraw);
// Validate gameId parameter
if (!gameId) {
throw new Error('gameId parameter is required');
}
if (gameId.trim() === '') {
throw new Error('gameId cannot be empty');
}
// Validate move parameter
if (!move) {
throw new Error('move parameter is required');
}
if (move.trim() === '') {
throw new Error('move cannot be empty');
}
try {
const params = new URLSearchParams();
if (offeringDraw) {
params.append('offeringDraw', 'true');
}
const response = await lichessRequest(`/board/game/${gameId}/move/${move}?${params.toString()}`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Game ${gameId} not found`);
}
if (response.status === 400) {
throw new Error('Invalid move');
}
throw new Error(`Failed to make move: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Move ${move} made in game ${gameId}${offeringDraw ? ' with draw offer' : ''}`
}]
};
} catch (error: any) {
throw new Error(`Failed to make move: ${error.message || 'Unknown error'}`);
}
}
case "abort_board_game": {
const gameId = String(request.params.arguments?.gameId);
// Validate gameId parameter
if (!gameId) {
throw new Error('gameId parameter is required');
}
if (gameId.trim() === '') {
throw new Error('gameId cannot be empty');
}
try {
const response = await lichessRequest(`/board/game/${gameId}/abort`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Game ${gameId} not found`);
}
if (response.status === 400) {
throw new Error('Game cannot be aborted');
}
throw new Error(`Failed to abort game: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Game ${gameId} aborted`
}]
};
} catch (error: any) {
throw new Error(`Failed to abort game: ${error.message || 'Unknown error'}`);
}
}
case "resign_board_game": {
const gameId = String(request.params.arguments?.gameId);
// Validate gameId parameter
if (!gameId) {
throw new Error('gameId parameter is required');
}
if (gameId.trim() === '') {
throw new Error('gameId cannot be empty');
}
try {
const response = await lichessRequest(`/board/game/${gameId}/resign`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Game ${gameId} not found`);
}
if (response.status === 400) {
throw new Error('Game cannot be resigned');
}
throw new Error(`Failed to resign game: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Resigned game ${gameId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to resign game: ${error.message || 'Unknown error'}`);
}
}
case "write_in_chat": {
const gameId = String(request.params.arguments?.gameId);
const room = String(request.params.arguments?.room);
const text = String(request.params.arguments?.text);
// Validate gameId parameter
if (!gameId) {
throw new Error('gameId parameter is required');
}
if (gameId.trim() === '') {
throw new Error('gameId cannot be empty');
}
// Validate room parameter
if (!room) {
throw new Error('room parameter is required');
}
if (!['player', 'spectator'].includes(room)) {
throw new Error('room must be either "player" or "spectator"');
}
// Validate text parameter
if (!text) {
throw new Error('text parameter is required');
}
if (text.trim() === '') {
throw new Error('text cannot be empty');
}
try {
const response = await lichessRequest(`/board/game/${gameId}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ room, text })
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Game ${gameId} not found`);
}
if (response.status === 400) {
throw new Error('Invalid chat message');
}
throw new Error(`Failed to send message: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Message sent to ${room} chat in game ${gameId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to send message: ${error.message || 'Unknown error'}`);
}
}
case "handle_draw_board_game": {
const gameId = String(request.params.arguments?.gameId);
const accept = Boolean(request.params.arguments?.accept ?? true);
// Validate gameId parameter
if (!gameId) {
throw new Error('gameId parameter is required');
}
if (gameId.trim() === '') {
throw new Error('gameId cannot be empty');
}
try {
const response = await lichessRequest(`/board/game/${gameId}/draw/${accept ? 'yes' : 'no'}`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Game ${gameId} not found`);
}
if (response.status === 400) {
throw new Error('No draw offer to handle');
}
throw new Error(`Failed to handle draw offer: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Draw offer ${accept ? 'accepted' : 'declined'} for game ${gameId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to handle draw offer: ${error.message || 'Unknown error'}`);
}
}
case "claim_victory": {
const gameId = String(request.params.arguments?.gameId);
// Validate gameId parameter
if (!gameId) {
throw new Error('gameId parameter is required');
}
if (gameId.trim() === '') {
throw new Error('gameId cannot be empty');
}
try {
const response = await lichessRequest(`/board/game/${gameId}/claim-victory`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Game ${gameId} not found`);
}
if (response.status === 400) {
throw new Error('Victory cannot be claimed');
}
throw new Error(`Failed to claim victory: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Victory claimed for game ${gameId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to claim victory: ${error.message || 'Unknown error'}`);
}
}
case "list_challenges": {
try {
const response = await lichessRequest('/challenge');
if (!response.ok) {
throw new Error(`Failed to list challenges: ${response.statusText}`);
}
// The API returns a list of challenges in JSON format
const challenges = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(challenges, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to list challenges: ${error.message || 'Unknown error'}`);
}
}
case "accept_challenge": {
const challengeId = String(request.params.arguments?.challengeId);
// Validate challengeId parameter
if (!challengeId) {
throw new Error('challengeId parameter is required');
}
if (challengeId.trim() === '') {
throw new Error('challengeId cannot be empty');
}
try {
const response = await lichessRequest(`/challenge/${challengeId}/accept`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Challenge ${challengeId} not found`);
}
if (response.status === 400) {
throw new Error('Challenge cannot be accepted');
}
throw new Error(`Failed to accept challenge: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Challenge ${challengeId} accepted`
}]
};
} catch (error: any) {
throw new Error(`Failed to accept challenge: ${error.message || 'Unknown error'}`);
}
}
case "decline_challenge": {
const challengeId = String(request.params.arguments?.challengeId);
const reason = String(request.params.arguments?.reason || 'generic');
// Validate challengeId parameter
if (!challengeId) {
throw new Error('challengeId parameter is required');
}
if (challengeId.trim() === '') {
throw new Error('challengeId cannot be empty');
}
// Validate reason parameter
const validReasons = ['generic', 'later', 'tooFast', 'tooSlow', 'timeControl', 'rated', 'casual', 'standard', 'variant', 'noBot', 'onlyBot'];
if (!validReasons.includes(reason)) {
throw new Error(`Invalid reason. Must be one of: ${validReasons.join(', ')}`);
}
try {
const response = await lichessRequest(`/challenge/${challengeId}/decline`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ reason })
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Challenge ${challengeId} not found`);
}
if (response.status === 400) {
throw new Error('Challenge cannot be declined');
}
throw new Error(`Failed to decline challenge: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Challenge ${challengeId} declined`
}]
};
} catch (error: any) {
throw new Error(`Failed to decline challenge: ${error.message || 'Unknown error'}`);
}
}
case "cancel_challenge": {
const challengeId = String(request.params.arguments?.challengeId);
// Validate challengeId parameter
if (!challengeId) {
throw new Error('challengeId parameter is required');
}
if (challengeId.trim() === '') {
throw new Error('challengeId cannot be empty');
}
try {
const response = await lichessRequest(`/challenge/${challengeId}/cancel`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Challenge ${challengeId} not found`);
}
if (response.status === 400) {
throw new Error('Challenge cannot be cancelled');
}
throw new Error(`Failed to cancel challenge: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Challenge ${challengeId} cancelled`
}]
};
} catch (error: any) {
throw new Error(`Failed to cancel challenge: ${error.message || 'Unknown error'}`);
}
}
case "get_arena_tournaments": {
try {
const response = await lichessRequest('/tournament');
if (!response.ok) {
throw new Error(`Failed to get arena tournaments: ${response.statusText}`);
}
// The API returns a list of current tournaments
const tournaments = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(tournaments, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get arena tournaments: ${error.message || 'Unknown error'}`);
}
}
case "create_arena": {
// Validate required name parameter
if (!request.params.arguments?.name) {
throw new Error('name parameter is required');
}
try {
const body: Record<string, any> = {
name: String(request.params.arguments.name),
clockTime: Number(request.params.arguments?.clockTime) || 3,
clockIncrement: Number(request.params.arguments?.clockIncrement) || 2,
minutes: Number(request.params.arguments?.minutes) || 45,
waitMinutes: Number(request.params.arguments?.waitMinutes) || 5,
variant: String(request.params.arguments?.variant || 'standard'),
rated: Boolean(request.params.arguments?.rated ?? true),
berserkable: Boolean(request.params.arguments?.berserkable ?? true),
streakable: Boolean(request.params.arguments?.streakable ?? true),
hasChat: Boolean(request.params.arguments?.hasChat ?? true)
};
// Validate numeric parameters
if (body.clockTime < 0) {
throw new Error('clockTime must be positive');
}
if (body.clockIncrement < 0) {
throw new Error('clockIncrement must be positive');
}
if (body.minutes < 1) {
throw new Error('minutes must be at least 1');
}
if (body.waitMinutes < 1) {
throw new Error('waitMinutes must be at least 1');
}
// Validate variant
const validVariants = ['standard', 'chess960', 'crazyhouse', 'antichess', 'atomic', 'horde', 'kingOfTheHill', 'racingKings', 'threeCheck'];
if (!validVariants.includes(body.variant)) {
throw new Error(`Invalid variant. Must be one of: ${validVariants.join(', ')}`);
}
// Add optional parameters
if (request.params.arguments?.startDate) {
const startDate = Number(request.params.arguments.startDate);
if (isNaN(startDate)) {
throw new Error('startDate must be a valid timestamp');
}
body.startDate = startDate;
}
if (request.params.arguments?.position) {
body.position = String(request.params.arguments.position);
}
if (request.params.arguments?.description) {
body.description = String(request.params.arguments.description);
}
if (request.params.arguments?.conditions) {
body.conditions = request.params.arguments.conditions;
}
const response = await lichessRequest('/tournament', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
if (response.status === 400) {
throw new Error('Invalid tournament parameters');
}
throw new Error(`Failed to create tournament: ${response.statusText}`);
}
const tournament = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(tournament, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to create tournament: ${error.message || 'Unknown error'}`);
}
}
case "get_arena_info": {
const tournamentId = String(request.params.arguments?.tournamentId);
// Validate tournamentId parameter
if (!tournamentId) {
throw new Error('tournamentId parameter is required');
}
if (tournamentId.trim() === '') {
throw new Error('tournamentId cannot be empty');
}
try {
const response = await lichessRequest(`/tournament/${tournamentId}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Tournament ${tournamentId} not found`);
}
throw new Error(`Failed to get tournament info: ${response.statusText}`);
}
const info = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(info, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get tournament info: ${error.message || 'Unknown error'}`);
}
}
case "get_arena_games": {
const tournamentId = String(request.params.arguments?.tournamentId);
const response = await lichessRequest(`/tournament/${tournamentId}/games`);
const games = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(games, null, 2)
}]
};
}
case "get_arena_results": {
const tournamentId = String(request.params.arguments?.tournamentId);
// Validate tournamentId parameter
if (!tournamentId) {
throw new Error('tournamentId parameter is required');
}
if (tournamentId.trim() === '') {
throw new Error('tournamentId cannot be empty');
}
try {
// Add optional query parameters
const queryParams = new URLSearchParams();
if (request.params.arguments?.nb) {
queryParams.append('nb', String(request.params.arguments.nb));
}
if (request.params.arguments?.sheet) {
queryParams.append('sheet', String(request.params.arguments.sheet));
}
const url = `/tournament/${tournamentId}/results${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
const response = await lichessRequest(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Tournament ${tournamentId} not found`);
}
throw new Error(`Failed to get tournament results: ${response.statusText}`);
}
const results = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get tournament results: ${error.message || 'Unknown error'}`);
}
}
case "join_arena": {
const tournamentId = String(request.params.arguments?.tournamentId);
// Validate tournamentId parameter
if (!tournamentId) {
throw new Error('tournamentId parameter is required');
}
if (tournamentId.trim() === '') {
throw new Error('tournamentId cannot be empty');
}
try {
const response = await lichessRequest(`/tournament/${tournamentId}/join`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Tournament ${tournamentId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to join this tournament');
}
if (response.status === 400) {
throw new Error('Cannot join this tournament');
}
throw new Error(`Failed to join tournament: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully joined tournament ${tournamentId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to join tournament: ${error.message || 'Unknown error'}`);
}
}
case "withdraw_from_arena": {
const tournamentId = String(request.params.arguments?.tournamentId);
// Validate tournamentId parameter
if (!tournamentId) {
throw new Error('tournamentId parameter is required');
}
if (tournamentId.trim() === '') {
throw new Error('tournamentId cannot be empty');
}
try {
const response = await lichessRequest(`/tournament/${tournamentId}/withdraw`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Tournament ${tournamentId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to withdraw from this tournament');
}
if (response.status === 400) {
throw new Error('Cannot withdraw from this tournament');
}
throw new Error(`Failed to withdraw from tournament: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully withdrew from tournament ${tournamentId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to withdraw from tournament: ${error.message || 'Unknown error'}`);
}
}
case "get_team_battle_results": {
const tournamentId = String(request.params.arguments?.tournamentId);
// Validate tournamentId parameter
if (!tournamentId) {
throw new Error('tournamentId parameter is required');
}
if (tournamentId.trim() === '') {
throw new Error('tournamentId cannot be empty');
}
try {
const response = await lichessRequest(`/tournament/${tournamentId}/teams`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Tournament ${tournamentId} not found`);
}
throw new Error(`Failed to get team battle results: ${response.statusText}`);
}
const results = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get team battle results: ${error.message || 'Unknown error'}`);
}
}
case "create_swiss": {
// Validate required parameters
if (!request.params.arguments?.name) {
throw new Error('name parameter is required');
}
if (!request.params.arguments?.teamId) {
throw new Error('teamId parameter is required');
}
const clock = request.params.arguments?.clock as { limit?: number; increment?: number } | undefined;
if (!clock) {
throw new Error('clock parameter is required');
}
if (!clock.limit || !clock.increment) {
throw new Error('clock must specify both limit and increment');
}
try {
const body: Record<string, any> = {
name: String(request.params.arguments.name),
teamId: String(request.params.arguments.teamId),
clock: {
limit: Number(clock.limit),
increment: Number(clock.increment)
},
nbRounds: Number(request.params.arguments?.nbRounds) || 7,
variant: String(request.params.arguments?.variant || 'standard'),
rated: Boolean(request.params.arguments?.rated ?? true),
roundInterval: Number(request.params.arguments?.roundInterval) || 300
};
// Validate numeric parameters
if (body.nbRounds < 1) {
throw new Error('nbRounds must be at least 1');
}
if (body.roundInterval < 1) {
throw new Error('roundInterval must be at least 1 second');
}
if (body.clock.limit < 0) {
throw new Error('clock limit must be positive');
}
if (body.clock.increment < 0) {
throw new Error('clock increment must be positive');
}
// Validate variant
const validVariants = ['standard', 'chess960', 'crazyhouse', 'antichess', 'atomic', 'horde', 'kingOfTheHill', 'racingKings', 'threeCheck'];
if (!validVariants.includes(body.variant)) {
throw new Error(`Invalid variant. Must be one of: ${validVariants.join(', ')}`);
}
// Add optional description
if (request.params.arguments?.description) {
body.description = String(request.params.arguments.description);
}
const response = await lichessRequest('/swiss/new', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
if (response.status === 404) {
throw new Error('Team not found');
}
if (response.status === 403) {
throw new Error('You are not allowed to create tournaments for this team');
}
throw new Error(`Failed to create Swiss tournament: ${response.statusText}`);
}
const tournament = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(tournament, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to create Swiss tournament: ${error.message || 'Unknown error'}`);
}
}
case "get_swiss_info": {
const swissId = String(request.params.arguments?.swissId);
// Validate swissId parameter
if (!swissId) {
throw new Error('swissId parameter is required');
}
if (swissId.trim() === '') {
throw new Error('swissId cannot be empty');
}
try {
const response = await lichessRequest(`/swiss/${swissId}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Swiss tournament ${swissId} not found`);
}
throw new Error(`Failed to get Swiss tournament info: ${response.statusText}`);
}
const info = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(info, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get Swiss tournament info: ${error.message || 'Unknown error'}`);
}
}
case "get_swiss_games": {
const swissId = String(request.params.arguments?.swissId);
// Validate swissId parameter
if (!swissId) {
throw new Error('swissId parameter is required');
}
if (swissId.trim() === '') {
throw new Error('swissId cannot be empty');
}
try {
// Build query parameters
const params = new URLSearchParams();
// Add optional parameters if provided
if (request.params.arguments?.player) {
params.append('player', String(request.params.arguments.player));
}
const booleanParams = ['moves', 'pgnInJson', 'tags', 'clocks', 'evals', 'opening'];
for (const param of booleanParams) {
if (request.params.arguments?.[param] !== undefined) {
params.append(param, String(request.params.arguments[param]));
}
}
const response = await lichessRequest(`/swiss/${swissId}/games?${params.toString()}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Swiss tournament ${swissId} not found`);
}
throw new Error(`Failed to get Swiss tournament games: ${response.statusText}`);
}
// Read the response as text since it may be NDJSON format
const text = await response.text();
// Split by newlines and parse each line as JSON
const games = text
.split('\n')
.filter(line => line.trim()) // Remove empty lines
.map(line => JSON.parse(line));
return {
content: [{
type: "text",
text: JSON.stringify(games, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get Swiss tournament games: ${error.message || 'Unknown error'}`);
}
}
case "get_swiss_results": {
const swissId = String(request.params.arguments?.swissId);
// Validate swissId parameter
if (!swissId) {
throw new Error('swissId parameter is required');
}
if (swissId.trim() === '') {
throw new Error('swissId cannot be empty');
}
try {
const response = await lichessRequest(`/swiss/${swissId}/results`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Swiss tournament ${swissId} not found`);
}
throw new Error(`Failed to get Swiss tournament results: ${response.statusText}`);
}
// Read the response as text and handle NDJSON format
const text = await response.text();
// Split by newlines and parse each line as JSON
const results = text
.split('\n')
.filter(line => line.trim()) // Remove empty lines
.map(line => JSON.parse(line));
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get Swiss tournament results: ${error.message || 'Unknown error'}`);
}
}
case "join_swiss": {
const swissId = String(request.params.arguments?.swissId);
// Validate swissId parameter
if (!swissId) {
throw new Error('swissId parameter is required');
}
if (swissId.trim() === '') {
throw new Error('swissId cannot be empty');
}
try {
const response = await lichessRequest(`/swiss/${swissId}/join`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Swiss tournament ${swissId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to join this tournament');
}
if (response.status === 400) {
throw new Error('Cannot join this tournament');
}
throw new Error(`Failed to join Swiss tournament: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully joined Swiss tournament ${swissId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to join Swiss tournament: ${error.message || 'Unknown error'}`);
}
}
case "withdraw_from_swiss": {
const swissId = String(request.params.arguments?.swissId);
// Validate swissId parameter
if (!swissId) {
throw new Error('swissId parameter is required');
}
if (swissId.trim() === '') {
throw new Error('swissId cannot be empty');
}
try {
const response = await lichessRequest(`/swiss/${swissId}/withdraw`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Swiss tournament ${swissId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to withdraw from this tournament');
}
if (response.status === 400) {
throw new Error('Cannot withdraw from this tournament');
}
throw new Error(`Failed to withdraw from Swiss tournament: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully withdrew from Swiss tournament ${swissId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to withdraw from Swiss tournament: ${error.message || 'Unknown error'}`);
}
}
case "get_current_simuls": {
try {
const response = await lichessRequest('/simul');
if (!response.ok) {
throw new Error(`Failed to get current simuls: ${response.statusText}`);
}
const simuls = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(simuls, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get current simuls: ${error.message || 'Unknown error'}`);
}
}
case "create_simul": {
// Validate required name parameter
if (!request.params.arguments?.name) {
throw new Error('name parameter is required');
}
try {
const body: Record<string, any> = {
name: String(request.params.arguments.name),
variant: String(request.params.arguments?.variant || 'standard'),
clockTime: Number(request.params.arguments?.clockTime) || 5,
clockIncrement: Number(request.params.arguments?.clockIncrement) || 3,
color: String(request.params.arguments?.color || 'white')
};
// Validate numeric parameters
if (body.clockTime < 0) {
throw new Error('clockTime must be positive');
}
if (body.clockIncrement < 0) {
throw new Error('clockIncrement must be positive');
}
// Validate variant
const validVariants = ['standard', 'chess960', 'crazyhouse', 'antichess', 'atomic', 'horde', 'kingOfTheHill', 'racingKings', 'threeCheck'];
if (!validVariants.includes(body.variant)) {
throw new Error(`Invalid variant. Must be one of: ${validVariants.join(', ')}`);
}
// Validate color
if (!['white', 'black'].includes(body.color)) {
throw new Error('color must be either "white" or "black"');
}
// Add optional parameters
if (request.params.arguments?.minRating) {
body.minRating = Number(request.params.arguments.minRating);
}
if (request.params.arguments?.maxRating) {
body.maxRating = Number(request.params.arguments.maxRating);
}
if (request.params.arguments?.text) {
body.text = String(request.params.arguments.text);
}
const response = await lichessRequest('/simul/new', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
if (response.status === 403) {
throw new Error('You are not allowed to create simuls');
}
throw new Error(`Failed to create simul: ${response.statusText}`);
}
const simul = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(simul, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to create simul: ${error.message || 'Unknown error'}`);
}
}
case "join_simul": {
const simulId = String(request.params.arguments?.simulId);
// Validate simulId parameter
if (!simulId) {
throw new Error('simulId parameter is required');
}
if (simulId.trim() === '') {
throw new Error('simulId cannot be empty');
}
try {
const response = await lichessRequest(`/simul/${simulId}/join`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Simul ${simulId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to join this simul');
}
if (response.status === 400) {
throw new Error('Cannot join this simul');
}
throw new Error(`Failed to join simul: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully joined simul ${simulId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to join simul: ${error.message || 'Unknown error'}`);
}
}
case "withdraw_from_simul": {
const simulId = String(request.params.arguments?.simulId);
// Validate simulId parameter
if (!simulId) {
throw new Error('simulId parameter is required');
}
if (simulId.trim() === '') {
throw new Error('simulId cannot be empty');
}
try {
const response = await lichessRequest(`/simul/${simulId}/withdraw`, {
method: 'POST'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Simul ${simulId} not found`);
}
if (response.status === 403) {
throw new Error('You are not allowed to withdraw from this simul');
}
if (response.status === 400) {
throw new Error('Cannot withdraw from this simul');
}
throw new Error(`Failed to withdraw from simul: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully withdrew from simul ${simulId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to withdraw from simul: ${error.message || 'Unknown error'}`);
}
}
case "export_study_chapter": {
const studyId = String(request.params.arguments?.studyId);
const chapterId = String(request.params.arguments?.chapterId);
// Validate IDs
if (!studyId || studyId.length !== 8) {
throw new Error('Study ID must be exactly 8 characters long');
}
if (!chapterId || chapterId.length !== 8) {
throw new Error('Chapter ID must be exactly 8 characters long');
}
// Build query parameters
const params = new URLSearchParams();
const booleanParams = ['clocks', 'comments', 'variations', 'source', 'orientation'];
for (const param of booleanParams) {
if (request.params.arguments?.[param] !== undefined) {
params.append(param, String(request.params.arguments[param]));
}
}
try {
const response = await lichessRequest(`/study/${studyId}/${chapterId}.pgn?${params.toString()}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Study chapter not found`);
}
throw new Error(`Failed to export study chapter: ${response.statusText}`);
}
const pgn = await response.text();
return {
content: [{
type: "text",
text: pgn
}]
};
} catch (error: any) {
throw new Error(`Failed to export study chapter: ${error.message || 'Unknown error'}`);
}
}
case "export_all_study_chapters": {
const studyId = String(request.params.arguments?.studyId);
// Validate studyId
if (!studyId || studyId.length !== 8) {
throw new Error('Study ID must be exactly 8 characters long');
}
// Build query parameters
const params = new URLSearchParams();
const booleanParams = ['clocks', 'comments', 'variations', 'source', 'orientation'];
for (const param of booleanParams) {
if (request.params.arguments?.[param] !== undefined) {
params.append(param, String(request.params.arguments[param]));
}
}
try {
const response = await lichessRequest(`/study/${studyId}.pgn?${params.toString()}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Study not found`);
}
throw new Error(`Failed to export study chapters: ${response.statusText}`);
}
const pgn = await response.text();
return {
content: [{
type: "text",
text: pgn
}]
};
} catch (error: any) {
throw new Error(`Failed to export study chapters: ${error.message || 'Unknown error'}`);
}
}
case "get_user_studies": {
const username = String(request.params.arguments?.username);
// Validate username
if (!username) {
throw new Error('Username parameter is required');
}
if (username.trim() === '') {
throw new Error('Username cannot be empty');
}
try {
const response = await lichessRequest(`/study/by/${username}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`User ${username} not found`);
}
throw new Error(`Failed to get user studies: ${response.statusText}`);
}
// Read the response as text and handle NDJSON format
const text = await response.text();
// Split by newlines and parse each line as JSON
const studies = text
.split('\n')
.filter(line => line.trim()) // Remove empty lines
.map(line => JSON.parse(line));
return {
content: [{
type: "text",
text: JSON.stringify(studies, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get user studies: ${error.message || 'Unknown error'}`);
}
}
case "get_thread": {
const userId = String(request.params.arguments?.userId);
// Validate userId
if (!userId) {
throw new Error('User ID parameter is required');
}
if (userId.trim() === '') {
throw new Error('User ID cannot be empty');
}
try {
const response = await lichessRequest(`/inbox/${userId}`);
if (!response.ok) {
if (response.status === 401) {
throw new Error('Missing authorization or insufficient permissions');
}
if (response.status === 404) {
throw new Error(`Thread with user ${userId} not found`);
}
throw new Error(`Failed to get thread: ${response.statusText}`);
}
const thread = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(thread, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get thread: ${error.message || 'Unknown error'}`);
}
}
case "get_official_broadcasts": {
try {
const response = await lichessRequest('/broadcast');
if (!response.ok) {
throw new Error(`Failed to get official broadcasts: ${response.statusText}`);
}
// Read the response as text
const text = await response.text();
// Split by newlines and parse each line as JSON
const broadcasts = text
.split('\n')
.filter(line => line.trim()) // Remove empty lines
.map(line => JSON.parse(line));
return {
content: [{
type: "text",
text: JSON.stringify(broadcasts, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get official broadcasts: ${error.message || 'Unknown error'}`);
}
}
case "get_broadcast": {
const broadcastId = String(request.params.arguments?.broadcastId);
// Validate broadcastId
if (!broadcastId) {
throw new Error('Broadcast ID parameter is required');
}
if (broadcastId.trim() === '') {
throw new Error('Broadcast ID cannot be empty');
}
try {
const response = await lichessRequest(`/broadcast/${broadcastId}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Broadcast ${broadcastId} not found`);
}
throw new Error(`Failed to get broadcast: ${response.statusText}`);
}
const broadcast = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(broadcast, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get broadcast: ${error.message || 'Unknown error'}`);
}
}
case "get_broadcast_round": {
const broadcastId = String(request.params.arguments?.broadcastId);
const roundId = String(request.params.arguments?.roundId);
// Validate IDs
if (!broadcastId) {
throw new Error('Broadcast ID parameter is required');
}
if (broadcastId.trim() === '') {
throw new Error('Broadcast ID cannot be empty');
}
if (!roundId) {
throw new Error('Round ID parameter is required');
}
if (roundId.trim() === '') {
throw new Error('Round ID cannot be empty');
}
try {
const response = await lichessRequest(`/broadcast/${broadcastId}/${roundId}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Broadcast round not found`);
}
throw new Error(`Failed to get broadcast round: ${response.statusText}`);
}
const round = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(round, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get broadcast round: ${error.message || 'Unknown error'}`);
}
}
case "push_broadcast_round_pgn": {
const broadcastId = String(request.params.arguments?.broadcastId);
const roundId = String(request.params.arguments?.roundId);
const pgn = String(request.params.arguments?.pgn);
// Validate parameters
if (!broadcastId) {
throw new Error('Broadcast ID parameter is required');
}
if (broadcastId.trim() === '') {
throw new Error('Broadcast ID cannot be empty');
}
if (!roundId) {
throw new Error('Round ID parameter is required');
}
if (roundId.trim() === '') {
throw new Error('Round ID cannot be empty');
}
if (!pgn) {
throw new Error('PGN parameter is required');
}
if (pgn.trim() === '') {
throw new Error('PGN cannot be empty');
}
try {
const response = await lichessRequest(`/broadcast/${broadcastId}/${roundId}/push`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: pgn
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Broadcast round not found`);
}
if (response.status === 401) {
throw new Error('Missing authorization or insufficient permissions');
}
throw new Error(`Failed to push PGN: ${response.statusText}`);
}
return {
content: [{
type: "text",
text: `Successfully pushed PGN to broadcast ${broadcastId} round ${roundId}`
}]
};
} catch (error: any) {
throw new Error(`Failed to push PGN: ${error.message || 'Unknown error'}`);
}
}
case "get_cloud_eval": {
const fen = String(request.params.arguments?.fen);
const multiPv = Number(request.params.arguments?.multiPv) || 1;
// Validate fen parameter
if (!fen) {
throw new Error('FEN parameter is required');
}
if (fen.trim() === '') {
throw new Error('FEN cannot be empty');
}
// Validate multiPv parameter
if (isNaN(multiPv)) {
throw new Error('multiPv must be a number');
}
if (multiPv < 1 || multiPv > 5) {
throw new Error('multiPv must be between 1 and 5');
}
try {
const params = new URLSearchParams({
fen,
multiPv: String(multiPv)
});
const response = await lichessRequest(`/cloud-eval?${params.toString()}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error('Position not found in cloud database');
}
throw new Error(`Failed to get cloud evaluation: ${response.statusText}`);
}
const evaluation = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(evaluation, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get cloud evaluation: ${error.message || 'Unknown error'}`);
}
}
case "get_fide_player": {
const playerId = String(request.params.arguments?.playerId); // Changed from username
// Validate playerId parameter
if (!playerId) {
throw new Error('FIDE player ID parameter is required');
}
if (playerId.trim() === '') {
throw new Error('FIDE player ID cannot be empty');
}
try {
const response = await lichessRequest(`/fide/player/${playerId}`); // Changed endpoint path
if (!response.ok) {
if (response.status === 404) {
throw new Error(`FIDE player ${playerId} not found`);
}
throw new Error(`Failed to get FIDE player info: ${response.statusText}`);
}
const profile = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(profile, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get FIDE player info: ${error.message || 'Unknown error'}`);
}
}
case "search_fide_players": {
const name = String(request.params.arguments?.name);
// Validate name parameter
if (!name) {
throw new Error('Name parameter is required');
}
if (name.trim() === '') {
throw new Error('Name cannot be empty');
}
try {
// The correct endpoint is /api/fide/player with query parameter 'q'
const response = await lichessRequest(`/fide/player?q=${encodeURIComponent(name)}`);
if (!response.ok) {
throw new Error(`Failed to search FIDE players: ${response.statusText}`);
}
const results = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to search FIDE players: ${error.message || 'Unknown error'}`);
}
}
case "get_ongoing_games": {
try {
const nb = Number(request.params.arguments?.nb) || 9;
// Validate nb parameter
if (isNaN(nb) || nb < 1 || nb > 50) {
throw new Error('nb parameter must be between 1 and 50');
}
const response = await lichessRequest(`/account/playing?nb=${nb}`);
if (!response.ok) {
throw new Error(`Failed to get ongoing games: ${response.statusText}`);
}
const games = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(games, null, 2)
}]
};
} catch (error: any) {
throw new Error(`Failed to get ongoing games: ${error.message || 'Unknown error'}`);
}
}
default:
throw new Error("Unknown tool");
}
});
/**
* Handler that lists available prompts
*/
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "analyze_position",
description: "Analyze the current position of a game",
}
]
};
});
/**
* Handler for the analyze_position prompt
*/
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
if (request.params.name !== "analyze_position") {
throw new Error("Unknown prompt");
}
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "Please analyze the current chess position and suggest the best moves for both sides. Consider:\n" +
"1. Material balance\n" +
"2. Piece activity\n" +
"3. King safety\n" +
"4. Pawn structure\n" +
"5. Tactical opportunities"
}
}
]
};
});
/**
* Start the server using stdio transport
*/
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});