Skip to main content
Glama

MonkeyType MCP Server

server.js18.2 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; const MONKEYTYPE_API_BASE_URL = 'https://api.monkeytype.com'; // Base schema that all tools will extend - no API key parameter, only using environment variable const BaseApiSchema = z.object({}); // User tools const CheckNameSchema = BaseApiSchema.extend({ name: z.string().describe("Username to check for availability") }); const GetPersonalBestsSchema = BaseApiSchema.extend({ mode: z.string().optional().describe("Mode for personal bests (time, words, quote, zen). Defaults to 'time'"), mode2: z.string().optional().describe("Secondary mode parameter for time mode (e.g., 15, 30, 60, 120). Defaults to '15'") }); const GetTagsSchema = BaseApiSchema.extend({}); const GetStatsSchema = BaseApiSchema.extend({}); const GetProfileSchema = BaseApiSchema.extend({ uidOrName: z.string().optional().describe("The UID or username of the user. If omitted or set to a keyword like 'me', 'self', 'current', or 'my', the value from the MONKEYTYPE_USERNAME environment variable will be used.") }); const SendForgotPasswordEmailSchema = BaseApiSchema.extend({ email: z.string().email().describe("Email address to send password reset link") }); const GetCurrentTestActivitySchema = BaseApiSchema.extend({}); const GetStreakSchema = BaseApiSchema.extend({}); // Test results tools const GetResultsSchema = BaseApiSchema.extend({ timestamp: z.number().optional().describe("Timestamp of the earliest result to fetch"), offset: z.number().optional().describe("Offset of the item at which to begin the response"), limit: z.number().optional().describe("Limit results to the given amount") }); const GetResultByIdSchema = BaseApiSchema.extend({ resultId: z.string().describe("ID of the result to retrieve") }); const GetLastResultSchema = BaseApiSchema.extend({}); // Public tools const GetSpeedHistogramSchema = BaseApiSchema.extend({ language: z.string().describe("Target language for the speed histogram (e.g., 'english')"), mode: z.enum(["time", "words", "quote", "custom", "zen"]).describe("Typing mode (e.g., 'time', 'words')"), mode2: z.string().describe("Secondary mode parameter (e.g., '60' for time mode)") }); const GetTypingStatsSchema = BaseApiSchema.extend({}); // Leaderboards tools const GetLeaderboardSchema = BaseApiSchema.extend({ language: z.string().describe("Target language for the leaderboard"), mode: z.enum(["time", "words", "quote", "custom", "zen"]).describe("Typing mode for the leaderboard"), mode2: z.string().describe("Secondary mode parameter"), page: z.number().int().min(0).optional().describe("Page number, 0-indexed. Default 0."), pageSize: z.number().int().min(10).max(200).optional().describe("Number of entries per page. Default 50, min 10, max 200.") }); const GetLeaderboardRankSchema = BaseApiSchema.extend({ language: z.string().describe("Language for the leaderboard"), mode: z.string().describe("Mode for the leaderboard (time, words, quote, zen)"), mode2: z.string().describe("Secondary mode parameter (e.g., 15, 60, etc.)") }); const GetDailyLeaderboardSchema = BaseApiSchema.extend({ language: z.string().optional().describe("Language for the leaderboard"), mode: z.string().optional().describe("Mode for the leaderboard (time, words, quote, zen)"), mode2: z.string().optional().describe("Secondary mode parameter (e.g., 15, 60, etc.)"), skip: z.number().optional().describe("Number of entries to skip"), limit: z.number().optional().describe("Number of entries to return") }); const GetWeeklyXpLeaderboardSchema = BaseApiSchema.extend({ skip: z.number().optional().describe("Number of entries to skip"), limit: z.number().optional().describe("Number of entries to return") }); // PSAs tools const GetPsasSchema = BaseApiSchema.extend({}); // Quotes tools const IsSubmissionEnabledSchema = BaseApiSchema.extend({}); // Server configuration tools const GetConfigurationSchema = BaseApiSchema.extend({}); // Server setup const server = new Server( { name: "monkeytype-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ); // Tool implementations async function callMonkeyTypeApi(endpoint, method, apiKey, params = {}, data = null) { try { const headers = { 'Authorization': `ApeKey ${apiKey}`, 'Content-Type': 'application/json', 'User-Agent': 'MonkeyType-MCP-Server/1.0.0' }; const config = { headers, timeout: 30000, validateStatus: status => status < 500 }; if (method === 'GET' && Object.keys(params).length > 0) { config.params = params; } let response; if (method === 'GET') { response = await axios.get(`${MONKEYTYPE_API_BASE_URL}${endpoint}`, config); } else if (method === 'POST') { response = await axios.post(`${MONKEYTYPE_API_BASE_URL}${endpoint}`, data, config); } return response.data; } catch (error) { console.error(`Error calling MonkeyType API: ${error.message}`); if (error.response) { return { error: error.response.data, statusCode: error.response.status }; } return { error: error.message }; } } // Expose tools to the LLM server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // User Tools { name: "check_username", description: "Check if a username is available on MonkeyType", inputSchema: zodToJsonSchema(CheckNameSchema), }, { name: "get_personal_bests", description: "Get user's personal best typing scores", inputSchema: zodToJsonSchema(GetPersonalBestsSchema), }, { name: "get_tags", description: "Get user's tags", inputSchema: zodToJsonSchema(GetTagsSchema), }, { name: "get_stats", description: "Get user's typing statistics", inputSchema: zodToJsonSchema(GetStatsSchema), }, { name: "get_profile", description: "Get user's profile information", inputSchema: zodToJsonSchema(GetProfileSchema), }, { name: "send_forgot_password_email", description: "Send a forgot password email to a user", inputSchema: zodToJsonSchema(SendForgotPasswordEmailSchema), }, { name: "get_current_test_activity", description: "Get user's current test activity", inputSchema: zodToJsonSchema(GetCurrentTestActivitySchema), }, { name: "get_streak", description: "Get user's typing streak information", inputSchema: zodToJsonSchema(GetStreakSchema), }, // Test Results Tools { name: "get_results", description: "Get user's typing test results", inputSchema: zodToJsonSchema(GetResultsSchema), }, { name: "get_result_by_id", description: "Get a specific typing test result by ID", inputSchema: zodToJsonSchema(GetResultByIdSchema), }, { name: "get_last_result", description: "Get user's last typing test result", inputSchema: zodToJsonSchema(GetLastResultSchema), }, // Public Tools { name: "get_speed_histogram", description: "Get speed histogram data", inputSchema: zodToJsonSchema(GetSpeedHistogramSchema), }, { name: "get_typing_stats", description: "Get global typing statistics", inputSchema: zodToJsonSchema(GetTypingStatsSchema), }, // Leaderboards Tools { name: "get_leaderboard", description: "Get typing test leaderboard", inputSchema: zodToJsonSchema(GetLeaderboardSchema), }, { name: "get_leaderboard_rank", description: "Get user's rank on the leaderboard", inputSchema: zodToJsonSchema(GetLeaderboardRankSchema), }, { name: "get_daily_leaderboard", description: "Get daily typing test leaderboard", inputSchema: zodToJsonSchema(GetDailyLeaderboardSchema), }, { name: "get_weekly_xp_leaderboard", description: "Get weekly XP leaderboard", inputSchema: zodToJsonSchema(GetWeeklyXpLeaderboardSchema), }, // PSAs Tools { name: "get_psas", description: "Get public service announcements", inputSchema: zodToJsonSchema(GetPsasSchema), }, // Quotes Tools { name: "is_submission_enabled", description: "Check if quote submission is enabled", inputSchema: zodToJsonSchema(IsSubmissionEnabledSchema), }, // Server Configuration Tools { name: "get_configuration", description: "Get server configuration", inputSchema: zodToJsonSchema(GetConfigurationSchema), }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; // Extract the API key from environment variable only const apiKey = process.env.MONKEYTYPE_API_KEY; if (!apiKey) { throw new Error("MONKEYTYPE_API_KEY environment variable is required. Please set it in your MCP server configuration."); } // Handle each tool switch (name) { // User Tools case "check_username": { const params = { name: args.name }; const result = await callMonkeyTypeApi(`/users/checkname`, 'GET', apiKey, params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_personal_bests": { // Add required mode parameter const params = { mode: args.mode || "time", // Default to time mode if not specified mode2: args.mode2 || "15" // Default to 15 seconds if not specified (confirmed from previous change) }; const result = await callMonkeyTypeApi('/users/personalBests', 'GET', apiKey, params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_tags": { const result = await callMonkeyTypeApi('/users/tags', 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_stats": { const result = await callMonkeyTypeApi('/users/stats', 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_profile": { let targetUidOrName; const keywordsForCurrentUser = ["me", "self", "current", "my"]; if (args.uidOrName) { if (keywordsForCurrentUser.includes(args.uidOrName.toLowerCase())) { targetUidOrName = process.env.MONKEYTYPE_USERNAME; if (!targetUidOrName) { throw new Error('uidOrName specified as current user, but MONKEYTYPE_USERNAME environment variable is not set.'); } } else { targetUidOrName = args.uidOrName; // Use the explicitly provided uidOrName } } else { // No uidOrName argument provided, try to use the environment variable targetUidOrName = process.env.MONKEYTYPE_USERNAME; if (!targetUidOrName) { throw new Error('uidOrName parameter is required, or MONKEYTYPE_USERNAME environment variable must be set.'); } } // Final check to ensure targetUidOrName is a non-empty string if (!targetUidOrName || typeof targetUidOrName !== 'string' || targetUidOrName.trim() === '') { throw new Error('Could not determine a valid UID/username. Please provide the uidOrName parameter or set the MONKEYTYPE_USERNAME environment variable with a non-empty value.'); } const result = await callMonkeyTypeApi(`/users/${targetUidOrName}/profile`, 'GET', apiKey, {}); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "send_forgot_password_email": { const result = await callMonkeyTypeApi('/users/forgotPassword', 'POST', apiKey, {}, { email: args.email }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_current_test_activity": { const result = await callMonkeyTypeApi('/users/activity/current', 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_streak": { const result = await callMonkeyTypeApi('/users/streak', 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } // Test Results Tools case "get_results": { const params = {}; if (args.timestamp) params.timestamp = args.timestamp; if (args.offset) params.offset = args.offset; if (args.limit) params.limit = args.limit; const result = await callMonkeyTypeApi('/results', 'GET', apiKey, params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_result_by_id": { const result = await callMonkeyTypeApi(`/results/${args.resultId}`, 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_last_result": { const result = await callMonkeyTypeApi('/results/last', 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } // Public Tools case "get_speed_histogram": { const params = { language: args.language, mode: args.mode, mode2: args.mode2 }; const result = await callMonkeyTypeApi('/public/speedHistogram', 'GET', apiKey, params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_typing_stats": { const result = await callMonkeyTypeApi('/public/typingStats', 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } // Leaderboards Tools case "get_leaderboard": { const params = { language: args.language, mode: args.mode, mode2: args.mode2 }; if (args.page !== undefined) params.page = args.page; if (args.pageSize !== undefined) params.pageSize = args.pageSize; const result = await callMonkeyTypeApi('/leaderboards', 'GET', apiKey, params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_leaderboard_rank": { const params = { language: args.language, mode: args.mode, mode2: args.mode2 }; const result = await callMonkeyTypeApi('/leaderboards/rank', 'GET', apiKey, params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_daily_leaderboard": { const params = {}; if (args.language) params.language = args.language; if (args.mode) params.mode = args.mode; if (args.mode2) params.mode2 = args.mode2; if (args.skip) params.skip = args.skip; if (args.limit) params.limit = args.limit; const result = await callMonkeyTypeApi('/leaderboards/daily', 'GET', apiKey, params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_weekly_xp_leaderboard": { const params = {}; if (args.skip) params.skip = args.skip; if (args.limit) params.limit = args.limit; const result = await callMonkeyTypeApi('/leaderboards/weeklyXp', 'GET', apiKey, params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } // PSAs Tools case "get_psas": { const result = await callMonkeyTypeApi('/psas', 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } // Quotes Tools case "is_submission_enabled": { const result = await callMonkeyTypeApi('/quotes/submission-enabled', 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } // Server Configuration Tools case "get_configuration": { const result = await callMonkeyTypeApi('/configuration', 'GET', apiKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true, }; } }); // Start server async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MonkeyType MCP Server running on stdio"); console.error("All MonkeyType API endpoints exposed as tools"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CodeDreamer06/MonkeytypeMCP'

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