Skip to content

Instantly share code, notes, and snippets.

@MarcinSwierczynski
Created May 17, 2025 13:47
Show Gist options
  • Save MarcinSwierczynski/3ee648d608ba23080098e64b74697914 to your computer and use it in GitHub Desktop.
Save MarcinSwierczynski/3ee648d608ba23080098e64b74697914 to your computer and use it in GitHub Desktop.
MCP Server for Markdown files browsing
#!/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