From db24c89c5ebc01c6d6945681e9aa2a0ba86163e1 Mon Sep 17 00:00:00 2001 From: Matt Herich Date: Fri, 21 Mar 2025 19:29:45 -0700 Subject: [PATCH 1/6] Add file read and directory listing enhancements - Add head/tail functionality for memory-efficient file reading - Implement new list_directory_with_sizes command with file size info - Add formatSize utility for human-readable file sizes --- src/filesystem/index.ts | 216 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 211 insertions(+), 5 deletions(-) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index b4d5c419..971928c0 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -97,6 +97,8 @@ async function validatePath(requestedPath: string): Promise { // Schema definitions const ReadFileArgsSchema = z.object({ path: z.string(), + tail: z.number().optional().describe('If provided, returns only the last N lines of the file'), + head: z.number().optional().describe('If provided, returns only the first N lines of the file') }); const ReadMultipleFilesArgsSchema = z.object({ @@ -127,6 +129,11 @@ const ListDirectoryArgsSchema = z.object({ path: z.string(), }); +const ListDirectoryWithSizesArgsSchema = z.object({ + path: z.string(), + sortBy: z.enum(['name', 'size']).optional().default('name').describe('Sort entries by name or size'), +}); + const DirectoryTreeArgsSchema = z.object({ path: z.string(), }); @@ -330,6 +337,107 @@ async function applyFileEdits( return formattedDiff; } +// Helper functions +function formatSize(bytes: number): string { + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) return '0 B'; + + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + if (i === 0) return `${bytes} ${units[i]}`; + + return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`; +} + +// Memory-efficient implementation to get the last N lines of a file +async function tailFile(filePath: string, numLines: number): Promise { + const CHUNK_SIZE = 1024; // Read 1KB at a time + const stats = await fs.stat(filePath); + const fileSize = stats.size; + + if (fileSize === 0) return ''; + + // Open file for reading + const fileHandle = await fs.open(filePath, 'r'); + try { + const lines: string[] = []; + let position = fileSize; + let chunk = Buffer.alloc(CHUNK_SIZE); + let linesFound = 0; + let remainingText = ''; + + // Read chunks from the end of the file until we have enough lines + while (position > 0 && linesFound < numLines) { + const size = Math.min(CHUNK_SIZE, position); + position -= size; + + const { bytesRead } = await fileHandle.read(chunk, 0, size, position); + if (!bytesRead) break; + + // Get the chunk as a string and prepend any remaining text from previous iteration + const readData = chunk.slice(0, bytesRead).toString('utf-8'); + const chunkText = readData + remainingText; + + // Split by newlines and count + const chunkLines = chunkText.split('\n'); + + // If this isn't the end of the file, the first line is likely incomplete + // Save it to prepend to the next chunk + if (position > 0) { + remainingText = chunkLines[0]; + chunkLines.shift(); // Remove the first (incomplete) line + } + + // Add lines to our result (up to the number we need) + for (let i = chunkLines.length - 1; i >= 0 && linesFound < numLines; i--) { + lines.unshift(chunkLines[i]); + linesFound++; + } + } + + return lines.join('\n'); + } finally { + await fileHandle.close(); + } +} + +// New function to get the first N lines of a file +async function headFile(filePath: string, numLines: number): Promise { + const fileHandle = await fs.open(filePath, 'r'); + try { + const lines: string[] = []; + let buffer = ''; + let bytesRead = 0; + const chunk = Buffer.alloc(1024); // 1KB buffer + + // Read chunks and count lines until we have enough or reach EOF + while (lines.length < numLines) { + const result = await fileHandle.read(chunk, 0, chunk.length, bytesRead); + if (result.bytesRead === 0) break; // End of file + bytesRead += result.bytesRead; + buffer += chunk.slice(0, result.bytesRead).toString('utf-8'); + + const newLineIndex = buffer.lastIndexOf('\n'); + if (newLineIndex !== -1) { + const completeLines = buffer.slice(0, newLineIndex).split('\n'); + buffer = buffer.slice(newLineIndex + 1); + for (const line of completeLines) { + lines.push(line); + if (lines.length >= numLines) break; + } + } + } + + // If there is leftover content and we still need lines, add it + if (buffer.length > 0 && lines.length < numLines) { + lines.push(buffer); + } + + return lines.join('\n'); + } finally { + await fileHandle.close(); + } +} + // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { @@ -340,7 +448,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { "Read the complete contents of a file from the file system. " + "Handles various text encodings and provides detailed error messages " + "if the file cannot be read. Use this tool when you need to examine " + - "the contents of a single file. Only works within allowed directories.", + "the contents of a single file. Use the 'head' parameter to read only " + + "the first N lines of a file, or the 'tail' parameter to read only " + + "the last N lines of a file. Only works within allowed directories.", inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput, }, { @@ -387,6 +497,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { "finding specific files within a directory. Only works within allowed directories.", inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput, }, + { + name: "list_directory_with_sizes", + description: + "Get a detailed listing of all files and directories in a specified path, including sizes. " + + "Results clearly distinguish between files and directories with [FILE] and [DIR] " + + "prefixes. This tool is useful for understanding directory structure and " + + "finding specific files within a directory. Only works within allowed directories.", + inputSchema: zodToJsonSchema(ListDirectoryWithSizesArgsSchema) as ToolInput, + }, { name: "directory_tree", description: @@ -451,6 +570,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { throw new Error(`Invalid arguments for read_file: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); + + if (parsed.data.head && parsed.data.tail) { + throw new Error("Cannot specify both head and tail parameters simultaneously"); + } + + if (parsed.data.tail) { + // Use memory-efficient tail implementation for large files + const tailContent = await tailFile(validPath, parsed.data.tail); + return { + content: [{ type: "text", text: tailContent }], + }; + } + + if (parsed.data.head) { + // Use memory-efficient head implementation for large files + const headContent = await headFile(validPath, parsed.data.head); + return { + content: [{ type: "text", text: headContent }], + }; + } + const content = await fs.readFile(validPath, "utf-8"); return { content: [{ type: "text", text: content }], @@ -530,11 +670,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } - case "directory_tree": { - const parsed = DirectoryTreeArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`); + case "list_directory_with_sizes": { + const parsed = ListDirectoryWithSizesArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for list_directory_with_sizes: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const entries = await fs.readdir(validPath, { withFileTypes: true }); + + // Get detailed information for each entry + const detailedEntries = await Promise.all( + entries.map(async (entry) => { + const entryPath = path.join(validPath, entry.name); + try { + const stats = await fs.stat(entryPath); + return { + name: entry.name, + isDirectory: entry.isDirectory(), + size: stats.size, + mtime: stats.mtime + }; + } catch (error) { + return { + name: entry.name, + isDirectory: entry.isDirectory(), + size: 0, + mtime: new Date(0) + }; } + }) + ); + + // Sort entries based on sortBy parameter + const sortedEntries = [...detailedEntries].sort((a, b) => { + if (parsed.data.sortBy === 'size') { + return b.size - a.size; // Descending by size + } + // Default sort by name + return a.name.localeCompare(b.name); + }); + + // Format the output + const formattedEntries = sortedEntries.map(entry => + `${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${ + entry.isDirectory ? "" : formatSize(entry.size).padStart(10) + }` + ); + + // Add summary + const totalFiles = detailedEntries.filter(e => !e.isDirectory).length; + const totalDirs = detailedEntries.filter(e => e.isDirectory).length; + const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0); + + const summary = [ + "", + `Total: ${totalFiles} files, ${totalDirs} directories`, + `Combined size: ${formatSize(totalSize)}` + ]; + + return { + content: [{ + type: "text", + text: [...formattedEntries, ...summary].join("\n") + }], + }; + } + + case "directory_tree": { + const parsed = DirectoryTreeArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`); + } interface TreeEntry { name: string; From fa002daa588e64234531a3164a1b852c0e0c1522 Mon Sep 17 00:00:00 2001 From: ChunHao Huang Date: Sat, 29 Mar 2025 11:33:52 +0000 Subject: [PATCH 2/6] Fix json format --- src/git/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/git/README.md b/src/git/README.md index 827d58fa..c98eb0b8 100644 --- a/src/git/README.md +++ b/src/git/README.md @@ -240,12 +240,13 @@ If you are doing local development, there are two ways to test your changes: "mcpServers": { "git": { "command": "uv", - "args": [ + "args": [ "--directory", "//mcp-servers/src/git", "run", "mcp-server-git" ] + } } } ``` From f41565ce8324280fcb1a56b275e14d51f4deb7c2 Mon Sep 17 00:00:00 2001 From: Matt Herich Date: Thu, 3 Apr 2025 00:31:26 -0700 Subject: [PATCH 3/6] Normalize line endings when splitting file chunks --- src/filesystem/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 971928c0..b78b9532 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -378,7 +378,7 @@ async function tailFile(filePath: string, numLines: number): Promise { const chunkText = readData + remainingText; // Split by newlines and count - const chunkLines = chunkText.split('\n'); + const chunkLines = normalizeLineEndings(chunkText).split('\n'); // If this isn't the end of the file, the first line is likely incomplete // Save it to prepend to the next chunk From 3fc5420e8036919e695cbb856620327b9d7a5c7c Mon Sep 17 00:00:00 2001 From: Ethan Lee Date: Fri, 18 Apr 2025 16:11:20 -0700 Subject: [PATCH 4/6] Add ActionKit to third-party servers list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 38f1dada..37b77dd5 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ These servers aim to demonstrate MCP features and the TypeScript and Python SDKs Official integrations are maintained by companies building production ready MCP servers for their platforms. - 21st.dev Logo **[21st.dev Magic](https://github.com/21st-dev/magic-mcp)** - Create crafted UI components inspired by the best 21st.dev design engineers. +- Paragon Logo **[ActionKit by Paragon](https://github.com/useparagon/paragon-mcp)** - Connect to 130+ SaaS integrations (e.g. Slack, Salesforce, Gmail) with Paragon’s [ActionKit](https://www.useparagon.com/actionkit) API. - Adfin Logo **[Adfin](https://github.com/Adfin-Engineering/mcp-server-adfin)** - The only platform you need to get paid - all payments in one place, invoicing and accounting reconciliations with [Adfin](https://www.adfin.com/). - AgentQL Logo **[AgentQL](https://github.com/tinyfish-io/agentql-mcp)** - Enable AI agents to get structured data from unstructured web with [AgentQL](https://www.agentql.com/). - AgentRPC Logo **[AgentRPC](https://github.com/agentrpc/agentrpc)** - Connect to any function, any language, across network boundaries using [AgentRPC](https://www.agentrpc.com/). From 55c3c3da76b77ebe3dea6b1453b7e0950171e646 Mon Sep 17 00:00:00 2001 From: vincent-pli Date: Wed, 28 May 2025 15:12:05 +0800 Subject: [PATCH 5/6] add mco-cli-host to "client" section --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f50047fc..3703c9af 100644 --- a/README.md +++ b/README.md @@ -566,6 +566,7 @@ These are high-level frameworks that make it easier to build MCP servers or clie * **[codemirror-mcp](https://github.com/marimo-team/codemirror-mcp)** - CodeMirror extension that implements the Model Context Protocol (MCP) for resource mentions and prompt commands * **[Spring AI MCP Client](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html)** - Provides auto-configuration for MCP client functionality in Spring Boot applications. +* **[MCP CLI Client](https://github.com/vincent-pli/mcp-cli-host)** - A CLI host application that enables Large Language Models (LLMs) to interact with external tools through the Model Context Protocol (MCP). ## 📚 Resources From 1875099457daade8de346e1d0ef680a0345f320f Mon Sep 17 00:00:00 2001 From: wommel0 Date: Mon, 9 Jun 2025 10:14:43 +0200 Subject: [PATCH 6/6] Add SAP ABAP MCP Server SDK to "For Servers" within "Frameworks" --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2eef0712..56ad50bb 100644 --- a/README.md +++ b/README.md @@ -594,6 +594,7 @@ These are high-level frameworks that make it easier to build MCP servers or clie * **[Higress MCP Server Hosting](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/mcp-servers)** - A solution for hosting MCP Servers by extending the API Gateway (based on Envoy) with wasm plugins. * **[MCP-Framework](https://mcp-framework.com)** Build MCP servers with elegance and speed in Typescript. Comes with a CLI to create your project with `mcp create app`. Get started with your first server in under 5 minutes by **[Alex Andru](https://github.com/QuantGeekDev)** * **[Quarkus MCP Server SDK](https://github.com/quarkiverse/quarkus-mcp-server)** (Java) +* **[SAP ABAP MCP Server SDK](https://github.com/abap-ai/mcp)** - Build SAP ABAP based MCP servers. ABAP 7.52 based with 7.02 downport; runs on R/3 & S/4HANA on-premises, currently not cloud-ready. * **[Spring AI MCP Server](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html)** - Provides auto-configuration for setting up an MCP server in Spring Boot applications. * **[Template MCP Server](https://github.com/mcpdotdirect/template-mcp-server)** - A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure