Created
May 17, 2025 13:47
-
-
Save MarcinSwierczynski/3ee648d608ba23080098e64b74697914 to your computer and use it in GitHub Desktop.
MCP Server for Markdown files browsing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; | |
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | |
import { promises as fs } from 'fs'; | |
import { join, resolve, relative } from 'path'; | |
import { z } from 'zod'; | |
const log = (...args) => { | |
if (process.env.CONFIG_LOG_LEVEL?.toUpperCase() == "VERBOSE") { | |
console.error(...args); | |
} | |
}; | |
// Define the base directory where markdown files are located | |
const BASE_DIR = resolve(process.env.MARKDOWN_DIR || './markdown'); | |
// Create an MCP server | |
const server = new McpServer({ | |
name: "markdown-query-server", | |
version: "1.0.0" | |
}); | |
// Security check function to prevent directory traversal | |
function isPathSafe(path) { | |
const resolvedPath = resolve(path); | |
return resolvedPath.startsWith(BASE_DIR); | |
} | |
// Helper function to list markdown files in a directory (recursive) | |
async function listMarkdownFiles(dir = BASE_DIR) { | |
try { | |
const resolvedDir = resolve(dir); | |
// Security check | |
if (!isPathSafe(resolvedDir)) { | |
throw new Error("Access denied: Directory outside the allowed base"); | |
} | |
const files = await fs.readdir(resolvedDir, { withFileTypes: true }); | |
const markdownFiles = []; | |
for (const file of files) { | |
const fullPath = join(resolvedDir, file.name); | |
if (file.isFile() && file.name.endsWith('.md')) { | |
const relativePath = relative(BASE_DIR, fullPath); | |
markdownFiles.push({ | |
uri: `file://${fullPath}`, | |
name: file.name, | |
relativePath, | |
description: `Markdown file: ${relativePath}`, | |
mimeType: 'text/markdown' | |
}); | |
} else if (file.isDirectory()) { | |
// Recursively list files in subdirectories | |
const subDirFiles = await listMarkdownFiles(fullPath); | |
markdownFiles.push(...subDirFiles); | |
} | |
} | |
return markdownFiles; | |
} catch (error) { | |
console.error(`Error listing markdown files: ${error.message}`); | |
return []; | |
} | |
} | |
// Helper function to read a markdown file | |
async function readMarkdownFile(uri) { | |
try { | |
const filePath = uri.replace('file://', ''); | |
// Security check | |
if (!isPathSafe(filePath)) { | |
throw new Error("Access denied: File outside the allowed base"); | |
} | |
const stats = await fs.stat(filePath); | |
if (!stats.isFile()) { | |
throw new Error("Not a file"); | |
} | |
const content = await fs.readFile(filePath, 'utf-8'); | |
return { | |
uri: uri, | |
text: content, | |
mimeType: 'text/markdown' | |
}; | |
} catch (error) { | |
throw new Error(`Error reading markdown file: ${error.message}`); | |
} | |
} | |
// Register a resource for listing all markdown files | |
server.resource( | |
"list-markdown", | |
"markdown://list", | |
async (uri) => ({ | |
contents: [{ | |
uri: uri.href, | |
text: JSON.stringify(["docker.md"], null, 2), | |
mimeType: 'application/json' | |
}] | |
}) | |
); | |
// Register a resource template for reading markdown files | |
server.resource( | |
"read-markdown", | |
new ResourceTemplate("file://{filePath}", { list: true }), | |
async (uri) => { | |
const content = await readMarkdownFile(uri.href); | |
return { | |
contents: [{ | |
uri: uri.href, | |
text: content.text, | |
mimeType: 'text/markdown' | |
}] | |
}; | |
} | |
); | |
// Register a tool for searching within markdown files | |
server.tool( | |
"search-markdown", | |
{ | |
searchTerm: z.string().describe("The term to search for in markdown files"), | |
maxResults: z.number().optional().default(10).describe("Maximum number of results to return") | |
}, | |
async ({ searchTerm, maxResults = 10 }) => { | |
const allFiles = await listMarkdownFiles(); | |
const results = []; | |
for (const file of allFiles) { | |
if (results.length >= maxResults) break; | |
try { | |
const content = await readMarkdownFile(file.uri); | |
if (content.text.toLowerCase().includes(searchTerm.toLowerCase())) { | |
// Include a snippet of the matching content | |
const lowerText = content.text.toLowerCase(); | |
const index = lowerText.indexOf(searchTerm.toLowerCase()); | |
const start = Math.max(0, index - 50); | |
const end = Math.min(content.text.length, index + searchTerm.length + 50); | |
const snippet = content.text.substring(start, end); | |
results.push({ | |
...file, | |
snippet, | |
matchPosition: index | |
}); | |
} | |
} catch (error) { | |
console.error(`Error searching file ${file.uri}: ${error.message}`); | |
} | |
} | |
return { | |
content: [{ | |
type: "text", | |
text: `Found ${results.length} markdown files containing "${searchTerm}":\n\n${JSON.stringify(results, null, 2)}` | |
}] | |
}; | |
} | |
); | |
// Create the stdio transport and connect the server | |
const transport = new StdioServerTransport(); | |
server.connect(transport).catch(error => { | |
console.error(`Failed to connect MCP server: ${error.message}`); | |
process.exit(1); | |
}); | |
console.error("Markdown Query MCP Server started..."); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment