UNPKG

@travisjbeck/ch-sh-mcp

Version:

MCP Server for cht.sh integration with Cursor

136 lines (135 loc) 6.04 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const zod_1 = require("zod"); const axios_1 = __importDefault(require("axios")); const CHT_SH_BASE_URL = "https://cht.sh"; // Create the MCP server instance const server = new mcp_js_1.McpServer({ name: "cht.sh MCP Server", version: "1.0.0", description: "Provides access to the cht.sh cheat sheet service." }); // Define the cht.sh query tool server.tool("query_cheatsheet", { query: zod_1.z.string().describe("The search query for cht.sh (e.g., 'python list comprehension', 'go http server', 'git log options')"), options: zod_1.z.string().optional().describe("Optional cht.sh query options (e.g., 'Tq', 'Q', see cht.sh/:help for more)") }, async ({ query, options }) => { // cht.sh expects queries in the format: language/keyword1+keyword2 // For multi-word queries not specifying a language first, we'll join with '+' // For language-specific queries like "python list comprehension", it becomes "python/list+comprehension" let formattedQuery = query.trim(); const parts = formattedQuery.split(/\s+/); if (parts.length > 1) { const firstPartIsLanguage = await isKnownLanguage(parts[0]); // Heuristic check if (firstPartIsLanguage) { formattedQuery = parts[0] + "/" + parts.slice(1).join('+'); } else { formattedQuery = parts.join('+'); } } // Single word queries are fine as is, e.g., "ls" let url = `${CHT_SH_BASE_URL}/${formattedQuery}`; if (options) { url += `?${options.replace(/\s+/g, '')}`; } console.error(`[CHT.SH_MCP_SERVER] Fetching: ${url}`); try { const response = await axios_1.default.get(url, { headers: { 'User-Agent': 'curl/7.64.1' }, // cht.sh can sometimes return non-UTF8 characters that break JSON.parse in MCP SDK // Best effort to get plain text. responseType: 'text', transformResponse: [(data) => data] // Prevent axios from parsing JSON }); if (response.status === 200 && typeof response.data === 'string') { let content = response.data; const MAX_LENGTH = 15000; // Max characters if (content.length > MAX_LENGTH) { content = content.substring(0, MAX_LENGTH) + "\n... (truncated due to length)"; } return { content: [{ type: "text", // Sanitize for any potential non-standard characters if necessary, though responseType: text should handle most. text: `cht.sh result for "${query}"${options ? ` with options "${options}"` : ''}:\n\n${content}` }] }; } else { console.error(`[CHT.SH_MCP_SERVER] Error from cht.sh: Status ${response.status}, Data: ${response.data}`); return { isError: true, content: [{ type: "text", text: `Error fetching from cht.sh for "${query}": Server returned status ${response.status}.` }] }; } } catch (error) { console.error(`[CHT.SH_MCP_SERVER] Request error for "${query}": ${error.message}`); let errorMessage = error.message; if (error.response && error.response.status === 404) { errorMessage = `No cheat sheet found for "${query}" on cht.sh (404).`; } else if (error.response) { errorMessage = `Error from cht.sh: Status ${error.response.status}.`; } return { isError: true, content: [{ type: "text", text: errorMessage }] }; } }); // Heuristic to check if the first word of a query is a language // This is a simplified check; cht.sh has a more complex internal logic. async function isKnownLanguage(lang) { // Common languages, not exhaustive. cht.sh supports many more. const commonLanguages = ["python", "javascript", "js", "go", "rust", "java", "c", "cpp", "csharp", "php", "ruby", "perl", "swift", "kotlin", "scala", "lua", "bash", "sh", "sql", "html", "css"]; if (commonLanguages.includes(lang.toLowerCase())) { return true; } // Fallback: try to fetch cht.sh/lang/.list to see if it's a known language/topic page try { const response = await axios_1.default.get(`${CHT_SH_BASE_URL}/${lang}/.list`, { headers: { 'User-Agent': 'curl/7.64.1' }, timeout: 1000 }); return response.status === 200 && response.data.trim() !== ''; // If it returns content, assume it's a language/topic page } catch (error) { return false; // If error (e.g. 404), assume not a distinct language path } } // Main function to start the server async function main() { console.error("[CHT.SH_MCP_SERVER] Starting cht.sh MCP server..."); try { const transport = new stdio_js_1.StdioServerTransport(); await server.connect(transport); console.error("[CHT.SH_MCP_SERVER] Server connected and listening on stdio."); } catch (error) { console.error(`[CHT.SH_MCP_SERVER] Error during startup: ${error.message}`); process.exit(1); } } // Global error handlers process.on('uncaughtException', (error) => { console.error(`[CHT.SH_MCP_SERVER] Uncaught Exception: ${error.message}`); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error(`[CHT.SH_MCP_SERVER] Unhandled Rejection at: ${promise}, reason: ${reason}`); process.exit(1); }); // Start the server main();