Drupal-Modules-MCP MCP Server

#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; import * as cheerio from "cheerio"; import { Element } from "domhandler"; interface DrushCommand { name: string; description: string; usage?: string; arguments?: Array<{ name: string; description: string; required: boolean; }>; options?: Array<{ name: string; description: string; }>; examples?: string[]; aliases?: string[]; url: string; } class DrushCommandsServer { private server: Server; private baseUrl: string = ""; constructor() { this.server = new Server( { name: "drush-commands-mcp", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "get_command_info", description: "Get detailed information about a specific Drush command", inputSchema: { type: "object", properties: { command_name: { type: "string", description: "Name of the Drush command", }, version: { type: "string", description: "Drush version (e.g. '13.x'). Defaults to '13.x'", }, }, required: ["command_name"], }, }, ], })); this.server.setRequestHandler( CallToolRequestSchema, async (request) => { const version = (request.params.arguments as any)?.version || "13.x"; this.baseUrl = `https://www.drush.org/${version}/commands`; switch (request.params.name) { case "get_command_info": const args = request.params.arguments as { command_name: string; version?: string; }; if (!args.command_name) { throw new McpError( ErrorCode.InvalidParams, "Command name is required" ); } try { const commandInfo = await this.fetchCommandInfo( args.command_name ); return { content: [ { type: "text", text: JSON.stringify( commandInfo, null, 2 ), }, ], }; } catch (error) { if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `Failed to fetch command info: ${error.message}` ); } throw error; } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } } ); } private async fetchCommandInfo(commandName: string): Promise<DrushCommand> { const url = `${this.baseUrl}/${commandName}/`; const response = await axios.get(url); const $ = cheerio.load(response.data); // Get the main content div const $content = $("article.md-content__inner"); // Get description (first paragraph after h1) const description = $content.find("h1 + p").text().trim(); // Helper function to extract section content const extractSection = (title: string): cheerio.Cheerio<Element> => { const $section = $content .find(`h4[id="${title.toLowerCase()}"]`) .next("ul"); return $section.length ? $section : cheerio.load("")("ul"); }; // Helper function to process description const processDescription = (desc: string): string => { // Convert HTML to plain text desc = desc.replace(/<\/?[^>]+(>|$)/g, ""); // Remove leading/trailing periods and spaces desc = desc.replace(/^\.?\s*/, "").replace(/\.\s*$/, ""); // Add note about multiple values if description contains ellipsis if (desc.includes("...")) { desc += " (accepts multiple values)"; } // Extract default value if present const defaultMatch = desc.match(/\(defaults?\s+to\s+([^)]+)\)/i); if (defaultMatch) { const defaultValue = defaultMatch[1].trim(); // Move default value to end if it's not already there if (!desc.endsWith(defaultMatch[0])) { desc = desc.replace(defaultMatch[0], "") + ` (defaults to ${defaultValue})`; } } // Add period at the end if missing if (!desc.endsWith(")") && !desc.endsWith(".")) { desc += "."; } return desc; }; // Parse arguments section const args: DrushCommand["arguments"] = []; const $argsSection = extractSection("Arguments"); if ($argsSection.length) { $argsSection.find("li").each((index: number, li: Element) => { // Get the raw HTML and extract argument name and description const html = $(li).html() || ""; const strongMatch = html.match( /<strong>\[?([^\]]+?)\]?<\/strong>\s*(.*)/ ); if (strongMatch) { const name = strongMatch[1].replace(/\.$/, ""); const description = processDescription(strongMatch[2]); args.push({ name, description, required: !html.includes("["), }); } }); } // Parse options section const options: DrushCommand["options"] = []; const $optionsSection = extractSection("Options"); if ($optionsSection.length) { $optionsSection.find("li").each((index: number, li: Element) => { const text = $(li).text().trim(); // Match option name and description const match = text.match( /^\*\*\s+--([a-zA-Z0-9-]+)(?:=[^*]+)?\*\*\.?\s*(.*)$/ ); if (match) { options.push({ name: match[1], description: processDescription(match[2]), }); } }); } // Parse examples section const examples: string[] = []; const $examplesSection = extractSection("Examples"); if ($examplesSection.length) { $examplesSection.find("li").each((index: number, li: Element) => { const text = $(li).text().trim(); if (text) { examples.push(text); } }); } // Parse aliases section const aliases: string[] = []; const $aliasesSection = extractSection("Aliases"); if ($aliasesSection.length) { $aliasesSection.find("li").each((index: number, li: Element) => { const text = $(li).text().trim(); if (text) { aliases.push(text); } }); } return { name: commandName, description, arguments: args, options, examples, aliases, url, }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Drush Commands MCP server running on stdio"); } } const server = new DrushCommandsServer(); server.run().catch(console.error);