Skip to main content
Glama
server.js15.1 kB
import express from "express"; import bodyParser from "body-parser"; import { spawn } from "child_process"; const app = express(); const PORT = process.env.PORT || 8080; // Defaults (can be overridden via config or env) const DEFAULT_REPO = process.env.DOCDEX_REPO_PATH || "/source"; const DEFAULT_LOG = process.env.DOCDEX_LOG_LEVEL || "warn"; const DEFAULT_MAX_RESULTS = Number(process.env.DOCDEX_MAX_RESULTS || 8); // Parse JSON bodies app.use(bodyParser.json()); // --------------------------------------------------------------------------- // Serve .well-known (server card + icon) // --------------------------------------------------------------------------- // Smithery will hit e.g.: // https://server.smithery.ai/@bekirdag/docdex/.well-known/mcp/server-card.json // The repo is copied to /source, so we serve /source/.well-known. app.use("/.well-known", (req, res, next) => { // CORS for discovery endpoints res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); next(); }); app.use("/.well-known", express.static("/source/.well-known")); // --------------------------------------------------------------------------- // Child process management (docdexd mcp) // --------------------------------------------------------------------------- let child = null; let childConfig = null; // { repoPath, logLevel, maxResults } let stdoutBuffer = ""; const pending = new Map(); // id(string) -> { resolve, reject } function normalizeConfig(overrides = {}) { const repoPath = typeof overrides.repoPath === "string" && overrides.repoPath.trim() ? overrides.repoPath.trim() : DEFAULT_REPO; const logLevel = typeof overrides.logLevel === "string" && overrides.logLevel.trim() ? overrides.logLevel.trim() : DEFAULT_LOG; const maxRaw = overrides.maxResults ?? DEFAULT_MAX_RESULTS; const maxNum = Number(maxRaw); const maxResults = Number.isFinite(maxNum) && maxNum > 0 ? maxNum : DEFAULT_MAX_RESULTS; return { repoPath, logLevel, maxResults }; } function startDocdex(config) { const { repoPath, logLevel, maxResults } = config; console.log( `[Adapter] Starting docdexd mcp --repo=${repoPath} --log=${logLevel} --max-results=${maxResults}` ); const args = [ "mcp", "--repo", repoPath, "--log", logLevel, "--max-results", String(maxResults), ]; const proc = spawn("docdexd", args); proc.stderr.on("data", (data) => { console.error("[docdexd]", data.toString()); }); proc.stdout.on("data", (chunk) => { stdoutBuffer += chunk.toString(); let idx; while ((idx = stdoutBuffer.indexOf("\n")) !== -1) { const line = stdoutBuffer.slice(0, idx).trim(); stdoutBuffer = stdoutBuffer.slice(idx + 1); if (!line) continue; let msg; try { msg = JSON.parse(line); } catch (err) { console.error( "[Adapter] Failed to parse JSON from docdexd:", err, "line:", line ); continue; } if (Object.prototype.hasOwnProperty.call(msg, "id")) { const key = String(msg.id); const entry = pending.get(key); if (entry) { pending.delete(key); entry.resolve(msg); } else { console.log("[Adapter] Unmatched response from docdexd:", msg); } } else { // Notifications from docdexd console.log("[Adapter] Notification from docdexd:", msg); } } }); proc.on("exit", (code, signal) => { console.warn(`[Adapter] docdexd exited (code=${code}, signal=${signal})`); if (child === proc) { child = null; childConfig = null; stdoutBuffer = ""; } for (const { reject } of pending.values()) { reject(new Error("docdexd process exited")); } pending.clear(); }); child = proc; childConfig = config; return proc; } function ensureDocdex(configOverrides = {}) { const desired = normalizeConfig(configOverrides); if (child && !child.killed && childConfig) { const same = childConfig.repoPath === desired.repoPath && childConfig.logLevel === desired.logLevel && childConfig.maxResults === desired.maxResults; if (same) return child; console.log("[Adapter] Restarting docdexd with new config"); child.kill(); } return startDocdex(desired); } function decodeConfig(req) { const raw = req.query?.config; if (!raw) return {}; try { const json = Buffer.from(String(raw), "base64").toString("utf8"); const parsed = JSON.parse(json); if (parsed && typeof parsed === "object") return parsed; } catch (err) { console.error("[Adapter] Failed to decode config query param:", err); } return {}; } function sendRequestToDocdex(message, configOverrides = {}) { const proc = ensureDocdex(configOverrides); const id = message.id; const key = String(id); return new Promise((resolve, reject) => { pending.set(key, { resolve, reject }); try { const payload = JSON.stringify(message) + "\n"; proc.stdin.write(payload, (err) => { if (err) { console.error("[Adapter] Failed to write to docdexd stdin:", err); if (pending.has(key)) { pending.get(key).reject(err); pending.delete(key); } } }); } catch (err) { if (pending.has(key)) { pending.get(key).reject(err); pending.delete(key); } return; } // Simple timeout so we don't hang forever const timeoutMs = 15000; setTimeout(() => { if (pending.has(key)) { pending.get(key).reject( new Error("Timeout waiting for response from docdexd") ); pending.delete(key); } }, timeoutMs); }); } function sendNotificationToDocdex(message, configOverrides = {}) { const proc = ensureDocdex(configOverrides); try { const payload = JSON.stringify(message) + "\n"; proc.stdin.write(payload); } catch (err) { console.error("[Adapter] Failed to send notification to docdexd:", err); } } // --------------------------------------------------------------------------- // MCP prompts (implemented in adapter) // --------------------------------------------------------------------------- const DOCDEX_PROMPTS_LIST = { prompts: [ { name: "docdex_repo_search", description: "Plan how to use Docdex tools to answer a question about this repository.", arguments: [ { name: "question", description: "Question about this repository or its documentation.", required: true, }, ], }, ], }; function handlePromptsList(message) { return { jsonrpc: "2.0", id: message.id, result: DOCDEX_PROMPTS_LIST, }; } function handlePromptsCall(message) { const params = message.params || {}; const name = params.name; const args = params.arguments || {}; if (name !== "docdex_repo_search") { return { jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `Unknown prompt: ${name}`, }, }; } const question = String(args.question || "").trim(); const messages = [ { role: "system", content: [ { type: "text", text: "You have access to Docdex MCP tools for this repository. " + "Use docdex_search to find relevant docs, then docdex_open or docdex_files " + "for detailed context. Cite file paths in your answer.", }, ], }, { role: "user", content: [ { type: "text", text: `Question: ${question}\n\n` + "Plan:\n" + "1. Call docdex_search with a short query.\n" + "2. Skim summaries/snippets.\n" + "3. Use docdex_open/docdex_files for details.\n" + "4. Answer using only those docs.", }, ], }, ]; return { jsonrpc: "2.0", id: message.id, result: { messages }, }; } // --------------------------------------------------------------------------- // MCP resources (simple static help resource) // --------------------------------------------------------------------------- const DOCDEX_RESOURCES = [ { uri: "docdex://help", name: "Docdex MCP usage guide", description: "How to use docdex_search, docdex_index, docdex_files, docdex_open and docdex_stats.", mimeType: "text/markdown", }, ]; const DOCDEX_RESOURCE_CONTENT = { "docdex://help": { uri: "docdex://help", mimeType: "text/markdown", text: [ "# Docdex MCP Usage", "", "- Use `docdex_search` first to find relevant docs.", "- If results look stale, call `docdex_index` to rebuild the index.", "- Use `docdex_files` to browse indexed files.", "- Use `docdex_open` to read a specific file or line range.", "- Use `docdex_stats` to inspect index statistics.", "", "Keep queries short and focused. Include file paths in your answer when possible.", ].join("\n"), }, }; function handleResourcesList(message) { return { jsonrpc: "2.0", id: message.id, result: { resources: DOCDEX_RESOURCES }, }; } function handleResourcesRead(message) { const params = message.params || {}; const uris = params.uris || []; const contents = []; for (const uri of uris) { if (DOCDEX_RESOURCE_CONTENT[uri]) { contents.push(DOCDEX_RESOURCE_CONTENT[uri]); } } return { jsonrpc: "2.0", id: message.id, result: { contents }, }; } // --------------------------------------------------------------------------- // Tool annotations (for Smithery Tool Quality score) // --------------------------------------------------------------------------- function annotateTool(tool) { const base = tool.annotations || {}; switch (tool.name) { case "docdex_search": return { ...tool, annotations: { ...base, category: "search", preferred: true, purpose: "Search repository documentation with a natural-language query.", }, }; case "docdex_index": return { ...tool, annotations: { ...base, category: "maintenance", purpose: "Rebuild or refresh the Docdex index.", }, }; case "docdex_files": return { ...tool, annotations: { ...base, category: "navigation", purpose: "List indexed documentation files.", }, }; case "docdex_open": return { ...tool, annotations: { ...base, category: "navigation", purpose: "Open a specific file or line range from the index.", }, }; case "docdex_stats": return { ...tool, annotations: { ...base, category: "introspection", purpose: "Inspect index size and statistics.", }, }; default: return { ...tool, annotations: { ...base, category: "other", }, }; } } // --------------------------------------------------------------------------- // Streamable HTTP MCP endpoint // --------------------------------------------------------------------------- app.post("/mcp", async (req, res) => { const message = req.body; if (!message || message.jsonrpc !== "2.0") { return res.status(400).json({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid JSON-RPC 2.0 message", }, }); } const hasId = Object.prototype.hasOwnProperty.call(message, "id"); const method = message.method; const configOverrides = decodeConfig(req); // Notifications (no id) – fire-and-forget if (!hasId) { if (method) { sendNotificationToDocdex(message, configOverrides); } return res.status(204).end(); } // Prompts implemented in adapter if (method === "prompts/list") { return res.json(handlePromptsList(message)); } if (method === "prompts/call") { return res.json(handlePromptsCall(message)); } // Resources implemented in adapter if (method === "resources/list") { return res.json(handleResourcesList(message)); } if (method === "resources/read") { return res.json(handleResourcesRead(message)); } // Wrap tools/list to inject annotations if (method === "tools/list") { try { const response = await sendRequestToDocdex(message, configOverrides); if (response?.result?.tools && Array.isArray(response.result.tools)) { response.result.tools = response.result.tools.map(annotateTool); } return res.json(response); } catch (err) { console.error("[Adapter] tools/list error:", err); return res.status(500).json({ jsonrpc: "2.0", id: message.id, error: { code: -32000, message: err?.message || "Internal error in tools/list wrapper", }, }); } } // Everything else -> pure proxy to docdexd try { const response = await sendRequestToDocdex(message, configOverrides); return res.json(response); } catch (err) { console.error("[Adapter] /mcp error:", err); return res.status(500).json({ jsonrpc: "2.0", id: message.id, error: { code: -32000, message: err?.message || "Internal MCP adapter error", }, }); } }); // --------------------------------------------------------------------------- // Optional SSE transport (for compatibility) // --------------------------------------------------------------------------- app.get("/sse", (req, res) => { const configOverrides = decodeConfig(req); const proc = ensureDocdex(configOverrides); res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }); console.log("[Adapter] New SSE connection"); // Tell client where to POST messages res.write(`event: endpoint\ndata: /message\n\n`); const onStdout = (chunk) => { const lines = chunk.toString().split("\n"); for (const line of lines) { if (line.trim()) { res.write(`event: message\ndata: ${line}\n\n`); } } }; proc.stdout.on("data", onStdout); req.on("close", () => { console.log("[Adapter] SSE connection closed"); proc.stdout.off("data", onStdout); }); }); app.post("/message", (req, res) => { const configOverrides = decodeConfig(req); sendNotificationToDocdex(req.body, configOverrides); res.status(202).send("Accepted"); }); // --------------------------------------------------------------------------- // Start server // --------------------------------------------------------------------------- app.listen(PORT, () => { console.log(`Docdex MCP adapter listening on port ${PORT}`); console.log(`Default repo path: ${DEFAULT_REPO}`); });

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/bekirdag/docdex'

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