Skip to main content
Glama

MCP Documentation Service

index.js15.2 kB
#!/usr/bin/env node /** * MCP Docs Service * * A Model Context Protocol implementation for documentation management. * This service provides tools for reading, writing, and managing markdown documentation. */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; import path from "path"; import { zodToJsonSchema } from "zod-to-json-schema"; // Import our utilities import { safeLog, normalizePath } from "./utils/index.js"; // Import schemas import { ReadDocumentSchema, WriteDocumentSchema, EditDocumentSchema, ListDocumentsSchema, SearchDocumentsSchema, CheckDocumentationHealthSchema, CreateFolderSchema, MoveDocumentSchema, RenameDocumentSchema, UpdateNavigationOrderSchema, CreateSectionSchema, ValidateLinksSchema, ValidateMetadataSchema, } from "./schemas/index.js"; // Import handlers import { DocumentHandler, NavigationHandler, HealthCheckHandler, } from "./handlers/index.js"; // Command line argument parsing const args = process.argv.slice(2); let docsDir = path.join(process.cwd(), "docs"); let createDir = false; let runHealthCheck = false; // Parse arguments for (let i = 0; i < args.length; i++) { if (args[i] === "--docs-dir" && i + 1 < args.length) { docsDir = path.resolve(args[i + 1]); i++; } else if (args[i] === "--create-dir") { createDir = true; } else if (args[i] === "--health-check") { runHealthCheck = true; } else if (!args[i].startsWith("--")) { docsDir = path.resolve(args[i]); } } // Normalize path docsDir = normalizePath(docsDir); // Ensure docs directory exists async function ensureDocsDirectory() { try { const stats = await fs.stat(docsDir); if (!stats.isDirectory()) { safeLog(`Error: ${docsDir} is not a directory`); process.exit(1); } } catch (error) { // Create directory if it doesn't exist try { await fs.mkdir(docsDir, { recursive: true }); safeLog(`Created docs directory: ${docsDir}`); // Create a sample README.md if --create-dir is specified if (createDir) { const readmePath = path.join(docsDir, "README.md"); try { await fs.access(readmePath); } catch { const content = `--- title: Documentation description: Project documentation --- # Documentation This is the documentation directory for your project. `; await fs.writeFile(readmePath, content); safeLog(`Created sample README.md in ${docsDir}`); } } return; } catch (error) { safeLog(`Error creating docs directory: ${error}`); process.exit(1); } } } // Initialize handlers const documentHandler = new DocumentHandler(docsDir); const navigationHandler = new NavigationHandler(docsDir); const healthCheckHandler = new HealthCheckHandler(docsDir); // Server setup const server = new Server({ name: "mcp-docs-service", version: "0.4.0", }, { capabilities: { tools: {}, }, }); // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "read_document", description: "Read a markdown document from the docs directory. Returns the document content " + "including frontmatter. Use this tool when you need to examine the contents of a " + "single document.", inputSchema: zodToJsonSchema(ReadDocumentSchema), }, { name: "write_document", description: "Create a new markdown document or completely overwrite an existing document with new content. " + "Use with caution as it will overwrite existing documents without warning. " + "Can create parent directories if they don't exist.", inputSchema: zodToJsonSchema(WriteDocumentSchema), }, { name: "edit_document", description: "Make line-based edits to a markdown document. Each edit replaces exact line sequences " + "with new content. Returns a git-style diff showing the changes made.", inputSchema: zodToJsonSchema(EditDocumentSchema), }, { name: "list_documents", description: "List all markdown documents in the docs directory or a subdirectory. " + "Returns the relative paths to all documents.", inputSchema: zodToJsonSchema(ListDocumentsSchema), }, { name: "search_documents", description: "Search for markdown documents containing specific text in their content or frontmatter. " + "Returns the relative paths to matching documents.", inputSchema: zodToJsonSchema(SearchDocumentsSchema), }, { name: "generate_documentation_navigation", description: "Generate a navigation structure from the markdown documents in the docs directory. " + "Returns a JSON structure that can be used for navigation menus.", inputSchema: zodToJsonSchema(ListDocumentsSchema), }, { name: "check_documentation_health", description: "Check the health of the documentation by analyzing frontmatter, links, and navigation. " + "Returns a report with issues and a health score. " + "Uses tolerance mode by default to provide a more forgiving health score for incomplete documentation.", inputSchema: zodToJsonSchema(CheckDocumentationHealthSchema), }, // New tools for Phase 2 { name: "create_documentation_folder", description: "Create a new folder in the docs directory. Optionally creates a README.md file " + "in the new folder with basic frontmatter.", inputSchema: zodToJsonSchema(CreateFolderSchema), }, { name: "move_document", description: "Move a document from one location to another. Optionally updates references to the " + "document in other files.", inputSchema: zodToJsonSchema(MoveDocumentSchema), }, { name: "rename_document", description: "Rename a document while preserving its location and content. Optionally updates " + "references to the document in other files.", inputSchema: zodToJsonSchema(RenameDocumentSchema), }, { name: "update_documentation_navigation_order", description: "Update the navigation order of a document by modifying its frontmatter.", inputSchema: zodToJsonSchema(UpdateNavigationOrderSchema), }, { name: "create_documentation_section", description: "Create a new navigation section with an index.md file.", inputSchema: zodToJsonSchema(CreateSectionSchema), }, // New tools for Phase 3 { name: "validate_documentation_links", description: "Check for broken internal links in documentation files.", inputSchema: zodToJsonSchema(ValidateLinksSchema), }, { name: "validate_documentation_metadata", description: "Ensure all documents have required metadata fields.", inputSchema: zodToJsonSchema(ValidateMetadataSchema), }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; switch (name) { case "read_document": { const parsed = ReadDocumentSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for read_document: ${parsed.error}`); } return await documentHandler.readDocument(parsed.data.path); } case "write_document": { const parsed = WriteDocumentSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for write_document: ${parsed.error}`); } return await documentHandler.writeDocument(parsed.data.path, parsed.data.content, parsed.data.createDirectories); } case "edit_document": { const parsed = EditDocumentSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for edit_document: ${parsed.error}`); } return await documentHandler.editDocument(parsed.data.path, parsed.data.edits, parsed.data.dryRun); } case "list_documents": { const parsed = ListDocumentsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for list_documents: ${parsed.error}`); } return await documentHandler.listDocuments(parsed.data.basePath, parsed.data.recursive); } case "search_documents": { const parsed = SearchDocumentsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for search_documents: ${parsed.error}`); } return await documentHandler.searchDocuments(parsed.data.query, parsed.data.basePath); } case "generate_documentation_navigation": { const parsed = ListDocumentsSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for generate_navigation: ${parsed.error}`); } return await navigationHandler.generateNavigation(parsed.data.basePath); } case "check_documentation_health": { const parsed = CheckDocumentationHealthSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for check_documentation_health: ${parsed.error}`); } return await healthCheckHandler.checkDocumentationHealth(parsed.data.basePath, { toleranceMode: parsed.data.toleranceMode }); } // New tools for Phase 2 case "create_documentation_folder": { const parsed = CreateFolderSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for create_folder: ${parsed.error}`); } return await documentHandler.createFolder(parsed.data.path, parsed.data.createReadme); } case "move_documentation_document": { const parsed = MoveDocumentSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for move_document: ${parsed.error}`); } return await documentHandler.moveDocument(parsed.data.sourcePath, parsed.data.destinationPath, parsed.data.updateReferences); } case "rename_documentation_document": { const parsed = RenameDocumentSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for rename_document: ${parsed.error}`); } return await documentHandler.renameDocument(parsed.data.path, parsed.data.newName, parsed.data.updateReferences); } case "update_documentation_navigation_order": { const parsed = UpdateNavigationOrderSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for update_navigation_order: ${parsed.error}`); } return await documentHandler.updateNavigationOrder(parsed.data.path, parsed.data.order); } case "create_section": { const parsed = CreateSectionSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for create_section: ${parsed.error}`); } return await documentHandler.createSection(parsed.data.title, parsed.data.path, parsed.data.order); } // New tools for Phase 3 case "validate_documentation_links": { const parsed = ValidateLinksSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for validate_links: ${parsed.error}`); } return await documentHandler.validateLinks(parsed.data.basePath, parsed.data.recursive); } case "validate_documentation_metadata": { const parsed = ValidateMetadataSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for validate_metadata: ${parsed.error}`); } return await documentHandler.validateMetadata(parsed.data.basePath, parsed.data.requiredFields); } 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, }; } }); // Run health check if requested if (runHealthCheck) { try { const result = await healthCheckHandler.checkDocumentationHealth(""); safeLog(result.content[0].text); process.exit(result.isError ? 1 : 0); } catch (error) { safeLog(`Error running health check: ${error}`); process.exit(1); } } else { // Start server (async () => { try { await ensureDocsDirectory(); const transport = new StdioServerTransport(); await server.connect(transport); safeLog("MCP Documentation Management Service started."); safeLog("Using docs directory:", docsDir); safeLog("Reading from stdin, writing results to stdout..."); } catch (error) { safeLog("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/alekspetrov/mcp-docs-service'

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