@travisjbeck/ch-sh-mcp
Version:
MCP Server for cht.sh integration with Cursor
136 lines (135 loc) • 6.04 kB
JavaScript
;
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();