📦 build: 初始化 Claude Code Skills 项目结构

添加 FileSystem MCP 服务器的完整实现,包括:
- 基于 MCP SDK 的 TypeScript 服务器
- 三个核心工具:read_file, list_directory, search_files
- 双传输模式支持(stdio 和 HTTP)
- 完整的类型安全和输入验证
- 项目文档和构建配置

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
dzq 2026-01-08 09:49:06 +08:00
commit f25d790d6f
14 changed files with 2928 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
# Skills bundled resources
.claude
# OS files
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
# OS
node_modules/

55
CLAUDE.md Normal file
View File

@ -0,0 +1,55 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a Claude Code Skills repository containing two main components:
1. **FileSystem MCP Server** (`filesystem-mcp-server/`) - A Model Context Protocol server implementation in TypeScript
2. **Claude Skills** (`.claude/skills/`) - 16 modular skills that extend Claude's capabilities
## Build Commands
All commands run from `filesystem-mcp-server/` directory:
```bash
# Build the TypeScript MCP server
npm run build
# Development mode with hot reload
npm run dev
# Start the server (after build)
npm start
# Clean build artifacts
npm run clean
```
**Environment Variables:**
- `TRANSPORT` - Transport mode (`stdio` or `http`, default: `http`)
- `PORT` - HTTP port (default: `3000`)
## Architecture
### MCP Server (`filesystem-mcp-server/`)
- **Transport**: Dual support (stdio for local, HTTP for remote)
- **Entry point**: `src/index.ts` - handles transport selection
- **Tools**: Modular pattern in `src/tools/` - each tool in its own file
- **Validation**: Zod schemas for all tool inputs
- **Type safety**: Strict TypeScript with `src/types.ts` and `src/constants.ts`
### Skills (`.claude/skills/`)
Each skill follows a standard structure:
- `SKILL.md` - Main documentation with YAML frontmatter (`name`, `description`)
- Optional: `scripts/`, `references/`, `assets/` folders
- Most skills reference Python libraries (pypdf, reportlab, python-pptx, openpyxl, etc.)
## Key Skills
- `mcp-builder` - Guides creating MCP servers with TypeScript/Python SDKs
- `skill-creator` - Guides creating new skills (6-step process)
## Important Notes
- No `package.json` at root - each component is separate
- MCP server uses ES modules (`"type": "module"`)
- No existing Cursor/Copilot rules files

19
filesystem-mcp-server/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Dependencies
node_modules/
# Build output
dist/
# OS files
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
npm-debug.log*

View File

@ -0,0 +1,266 @@
# FileSystem MCP Server
一个基于 Model Context Protocol (MCP) 的文件系统服务器,提供与本地文件系统交互的工具。
## 概述
FileSystem MCP Server 是一个 Node.js/TypeScript 实现的服务端应用,通过 MCP 协议使 AI 模型能够安全地读取、浏览和搜索本地文件系统。该服务器提供三个核心工具:`read_file`、`list_directory` 和 `search_files`,支持多种输出格式和输入验证。
## 功能特性
- **安全文件读取** - 支持指定编码、行范围和字符限制的文本读取
- **目录内容浏览** - 列出目录中的文件和子目录,包含元数据
- **模式匹配搜索** - 使用 glob 模式递归搜索文件
- **双传输模式** - 支持 stdio本地和 HTTP远程两种传输方式
- **类型安全** - 完整的 TypeScript 类型定义和 Zod 输入验证
## 系统要求
- Node.js >= 18
- npm 或 yarn
## 快速开始
### 安装
```bash
cd filesystem-mcp-server
npm install
```
### 构建
```bash
npm run build
```
### 运行
**stdio 模式(默认,本地集成)**
```bash
npm start
```
**HTTP 模式(远程访问)**
```bash
TRANSPORT=http npm start
```
**开发模式(热重载)**
```bash
npm run dev
```
HTTP 模式默认监听 3000 端口,可通过 `PORT` 环境变量指定其他端口:
```bash
PORT=8080 TRANSPORT=http npm start
```
## 工具说明
### read_file
读取指定路径的文件内容。
**参数**
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|------|------|------|--------|------|
| `path` | string | 是 | - | 文件的绝对路径 |
| `encoding` | string | 否 | `utf-8` | 文件编码 |
| `max_chars` | number | 否 | - | 返回的最大字符数 |
| `line_start` | number | 否 | - | 起始行号1-based |
| `line_end` | number | 否 | - | 结束行号1-based |
| `response_format` | string | 否 | `markdown` | 输出格式:`markdown` 或 `json` |
**示例**
```json
{
"path": "/path/to/config.json",
"encoding": "utf-8",
"line_start": 1,
"line_end": 100,
"response_format": "markdown"
}
```
**输出格式**
- Markdown人类可读的格式化输出包含文件信息和代码块
- JSON包含完整元数据的结构化数据
---
### list_directory
列出指定目录的内容。
**参数**
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|------|------|------|--------|------|
| `path` | string | 是 | - | 目录的绝对路径 |
| `response_format` | string | 否 | `markdown` | 输出格式:`markdown` 或 `json` |
**示例**
```json
{
"path": "/path/to/project",
"response_format": "markdown"
}
```
**输出格式**
- Markdown人类可读的目录结构使用图标标识文件和目录
- JSON包含文件和目录详细信息的结构化数据
---
### search_files
使用 glob 模式在目录中搜索文件。
**参数**
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|------|------|------|--------|------|
| `path` | string | 是 | - | 搜索起始目录的绝对路径 |
| `pattern` | string | 是 | - | glob 模式(如 `*.ts`、`**/*.json` |
| `max_results` | number | 否 | 50 | 返回的最大结果数(最大 100 |
| `include_content` | boolean | 否 | `false` | 是否在结果中包含文件内容 |
| `response_format` | string | 否 | `markdown` | 输出格式:`markdown` 或 `json` |
**模式示例**
| 模式 | 匹配 |
|------|------|
| `*.ts` | 当前目录下的所有 TypeScript 文件 |
| `**/*.json` | 任意目录下的所有 JSON 文件 |
| `src/**/*.ts` | src 目录下的所有 TypeScript 文件 |
| `*.{js,ts}` | 当前目录下的所有 JS 和 TS 文件 |
**示例**
```json
{
"path": "/path/to/project",
"pattern": "**/*.ts",
"max_results": 50,
"include_content": false,
"response_format": "json"
}
```
**输出格式**
- Markdown人类可读的搜索结果列表
- JSON包含所有匹配文件详细信息的结构化数据
## 环境变量
| 变量 | 值 | 说明 |
|------|-----|------|
| `TRANSPORT` | `stdio``http` | 传输协议,默认 `stdio` |
| `PORT` | 端口号 | HTTP 模式监听的端口,默认 `3000` |
## 在 MCP 客户端中使用
### Claude Desktop 配置
在 Claude Desktop 的配置文件中添加:
```json
{
"mcpServers": {
"filesystem": {
"command": "node",
"args": ["/path/to/filesystem-mcp-server/dist/index.js"],
"disabled": false,
"env": {}
}
}
}
```
### 环境变量配置
如需使用 HTTP 模式:
```json
{
"mcpServers": {
"filesystem": {
"command": "node",
"args": ["/path/to/filesystem-mcp-server/dist/index.js"],
"disabled": false,
"env": {
"TRANSPORT": "http",
"PORT": "3000"
}
}
}
}
```
## 项目结构
```
filesystem-mcp-server/
├── package.json # 项目配置和依赖
├── tsconfig.json # TypeScript 编译配置
├── README.md # 项目文档
├── src/
│ ├── index.ts # 服务器入口点
│ ├── types.ts # TypeScript 类型定义
│ ├── constants.ts # 常量配置
│ └── tools/
│ ├── index.ts # 工具注册
│ ├── read-file.ts # 读取文件工具实现
│ ├── list-directory.ts # 目录列表工具实现
│ └── search-files.ts # 文件搜索工具实现
└── dist/ # 编译输出
```
## 开发
### 可用脚本
```bash
# 构建项目
npm run build
# 开发模式(热重载)
npm run dev
# 清理构建产物
npm run clean
```
### 添加新工具
1. 在 `src/tools/` 目录中创建新的工具文件
2. 使用 `server.registerTool()` 注册工具
3. 在 `src/tools/index.ts` 中导出工具注册函数
## 错误处理
所有工具都提供清晰的错误信息:
| 错误类型 | 错误信息 |
|---------|---------|
| 文件不存在 | `Error: File not found: <path>` |
| 目录不存在 | `Error: Directory not found: <path>` |
| 权限被拒 | `Error: Permission denied to <operation>: <path>` |
| 路径是文件 | `Error: Path is a file, not a directory: <path>` |
| 路径是目录 | `Error: Path is a directory, not a file: <path>` |
## 许可证
MIT License

1793
filesystem-mcp-server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "filesystem-mcp-server",
"version": "1.0.0",
"description": "MCP server for filesystem operations",
"type": "module",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"build": "tsc",
"clean": "rm -rf dist"
},
"engines": {
"node": ">=18"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^22.10.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}

View File

@ -0,0 +1,11 @@
/**
* Constants for the FileSystem MCP Server.
*/
export const CHARACTER_LIMIT = 25000;
export const DEFAULT_ENCODING = "utf-8";
export const MAX_DEPTH = 10;
export const DEFAULT_FILE_LIMIT = 100;

View File

@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* FileSystem MCP Server.
*
* This server provides tools to interact with the local filesystem,
* including reading files, listing directories, and searching for files.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { registerAllTools } from "./tools/index.js";
// Create MCP server instance
const server = new McpServer({
name: "filesystem-mcp",
version: "1.0.0"
});
// Register all tools
registerAllTools(server);
// Run with stdio transport (default, for local integration)
async function runStdio() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("FileSystem MCP server running via stdio");
}
// Run with HTTP transport (for remote access)
async function runHTTP() {
const app = express();
app.use(express.json());
app.post('/mcp', async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
res.on('close', () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
const port = parseInt(process.env.PORT || '7666');
app.listen(port, () => {
console.error(`FileSystem MCP server running on http://localhost:${port}/mcp`);
});
}
// Choose transport based on environment
const transport = process.env.TRANSPORT || 'http';
if (transport === 'http') {
runHTTP().catch(error => {
console.error("Server error:", error);
process.exit(1);
});
} else {
runStdio().catch(error => {
console.error("Server error:", error);
process.exit(1);
});
}

View File

@ -0,0 +1,14 @@
/**
* Tool exports and registration for the FileSystem MCP Server.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerReadFileTool } from "./read-file.js";
import { registerListDirectoryTool } from "./list-directory.js";
import { registerSearchFilesTool } from "./search-files.js";
export function registerAllTools(server: McpServer) {
registerReadFileTool(server);
registerListDirectoryTool(server);
registerSearchFilesTool(server);
}

View File

@ -0,0 +1,193 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { promises as fs } from "fs";
import { lstat } from "fs/promises";
import { DEFAULT_FILE_LIMIT } from "../constants.js";
import type { DirectoryEntry } from "../types.js";
const ListDirectoryInputSchema = z.object({
path: z.string()
.min(1, "Path is required")
.describe("Absolute path to the directory to list"),
response_format: z.enum(["markdown", "json"])
.default("markdown")
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
}).strict();
type ListDirectoryInput = z.infer<typeof ListDirectoryInputSchema>;
function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
export function registerListDirectoryTool(server: McpServer) {
server.registerTool(
"list_directory",
{
title: "List Directory",
description: `List the contents of a directory in the filesystem.
This tool returns a list of all files and subdirectories within a specified directory. It provides metadata including file sizes and modification times.
Args:
- path (string): Absolute path to the directory to list
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns:
For JSON format: Structured data with schema:
{
"path": string, // Path to the directory
"total_items": number, // Total number of items
"directories": [ // List of subdirectories
{
"name": string,
"path": string,
"isDirectory": true,
"size": number, // Always 0 for directories
"modified": string // ISO date string
}
],
"files": [ // List of files
{
"name": string,
"path": string,
"isDirectory": false,
"size": number, // File size in bytes
"modified": string // ISO date string
}
]
}
Examples:
- Use when: "List all files in /etc" -> params with path="/etc"
- Use when: "Show contents of the project src folder" -> params with path="/path/to/project/src"
- Don't use when: You need to read file contents (use read_file instead)
- Don't use when: You need to search for specific files (use search_files instead)
Error Handling:
- Returns "Error: Directory not found" if the path does not exist (ENOENT)
- Returns "Error: Path is a file" if the path is a file, not a directory
- Returns "Error: Permission denied" if the directory cannot be read (EACCES)`,
inputSchema: ListDirectoryInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
},
async (params: ListDirectoryInput) => {
try {
// Check if path exists and get stats
const stats = await lstat(params.path);
if (!stats.isDirectory()) {
return {
content: [{
type: "text",
text: `Error: Path is a file, not a directory: ${params.path}`
}]
};
}
// Read directory contents
const entries = await fs.readdir(params.path, { withFileTypes: true });
const directories: DirectoryEntry[] = [];
const files: DirectoryEntry[] = [];
for (const entry of entries) {
const entryPath = `${params.path}/${entry.name}`;
const entryStats = await lstat(entryPath);
const entryInfo: DirectoryEntry = {
name: entry.name,
path: entryPath,
isDirectory: entry.isDirectory(),
size: entryStats.size,
modified: entryStats.mtime.toISOString()
};
if (entry.isDirectory()) {
directories.push(entryInfo);
} else {
files.push(entryInfo);
}
}
// Sort alphabetically
directories.sort((a, b) => a.name.localeCompare(b.name));
files.sort((a, b) => a.name.localeCompare(b.name));
const output = {
path: params.path,
total_items: directories.length + files.length,
directories,
files
};
let textContent: string;
if (params.response_format === "json") {
textContent = JSON.stringify(output, null, 2);
} else {
const lines: string[] = [
`# Directory: ${params.path}`,
"",
`Total: ${directories.length} directories, ${files.length} files`,
""
];
if (directories.length > 0) {
lines.push("## Directories", "");
for (const dir of directories) {
lines.push(`- 📁 ${dir.name}/`);
}
lines.push("");
}
if (files.length > 0) {
lines.push("## Files", "");
for (const file of files) {
lines.push(`- 📄 ${file.name} (${formatFileSize(file.size)})`);
}
lines.push("");
}
textContent = lines.join('\n');
}
return {
content: [{ type: "text", text: textContent }],
structuredContent: output
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
let errorText: string;
if (error instanceof Error && 'code' in error) {
const code = (error as { code: string }).code;
switch (code) {
case "ENOENT":
errorText = `Error: Directory not found: ${params.path}`;
break;
case "EACCES":
errorText = `Error: Permission denied to access directory: ${params.path}`;
break;
default:
errorText = `Error: Failed to list directory: ${errorMessage}`;
}
} else {
errorText = `Error: Failed to list directory: ${errorMessage}`;
}
return {
content: [{ type: "text", text: errorText }]
};
}
}
);
}

View File

@ -0,0 +1,181 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { promises as fs } from "fs";
import { stat } from "fs/promises";
import { DEFAULT_ENCODING, CHARACTER_LIMIT } from "../constants.js";
import type { ResponseFormat } from "../types.js";
const ReadFileInputSchema = z.object({
path: z.string()
.min(1, "Path is required")
.describe("Absolute path to the file to read"),
encoding: z.string()
.default(DEFAULT_ENCODING)
.describe("File encoding (default: utf-8)"),
max_chars: z.number()
.int()
.min(1)
.max(CHARACTER_LIMIT)
.optional()
.describe("Maximum number of characters to return"),
line_start: z.number()
.int()
.min(1)
.optional()
.describe("Starting line number (1-based)"),
line_end: z.number()
.int()
.min(1)
.optional()
.describe("Ending line number (1-based, inclusive)"),
response_format: z.enum(["markdown", "json"])
.default("markdown")
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
}).strict();
type ReadFileInput = z.infer<typeof ReadFileInputSchema>;
export function registerReadFileTool(server: McpServer) {
server.registerTool(
"read_file",
{
title: "Read File",
description: `Read the contents of a file from the filesystem.
This tool reads a file and returns its contents. For large files, use max_chars to limit the output, or use line_start and line_end to read specific line ranges.
Args:
- path (string): Absolute path to the file to read
- encoding (string): File encoding (default: utf-8). Common encodings: utf-8, ascii, base64
- max_chars (number): Maximum number of characters to return (optional)
- line_start (number): Starting line number, 1-based (optional)
- line_end (number): Ending line number, 1-based (optional)
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns:
For JSON format: Structured data with schema:
{
"path": string, // Path to the file
"size": number, // File size in bytes
"lines": number, // Total number of lines
"content": string, // File contents
"truncated": boolean, // Whether content was truncated
"partial_range": { // Present if reading partial file
"line_start": number,
"line_end": number
}
}
Examples:
- Use when: "Read the contents of config.json" -> params with path="/path/to/config.json"
- Use when: "Show first 100 lines of a large log file" -> params with path="/var/log/app.log", line_start=1, line_end=100
- Use when: "Get last 500 characters of a file" -> params with path="/path/to/file.txt", max_chars=500
Error Handling:
- Returns "Error: File not found" if the path does not exist (ENOENT)
- Returns "Error: Permission denied" if the file cannot be read (EACCES)
- Returns "Error: Path is a directory" if the path is a directory`,
inputSchema: ReadFileInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
},
async (params: ReadFileInput) => {
try {
// Check if path exists and get file stats
const fileStats = await stat(params.path);
if (fileStats.isDirectory()) {
return {
content: [{
type: "text",
text: `Error: Path is a directory, not a file: ${params.path}`
}]
};
}
// Read file content
const contentBuffer = await fs.readFile(params.path);
let content = contentBuffer.toString(params.encoding as BufferEncoding);
const totalLines = content.split('\n').length;
let truncated = false;
let partialRange: { line_start: number; line_end: number } | undefined;
// Handle line range filtering
if (params.line_start !== undefined || params.line_end !== undefined) {
const lines = content.split('\n');
const start = (params.line_start || 1) - 1;
const end = params.line_end ? params.line_end : lines.length;
content = lines.slice(start, end).join('\n');
partialRange = {
line_start: start + 1,
line_end: end
};
}
// Handle character limit
if (params.max_chars && content.length > params.max_chars) {
content = content.slice(0, params.max_chars);
truncated = true;
}
const output = {
path: params.path,
size: fileStats.size,
lines: totalLines,
content,
truncated,
...(partialRange && { partial_range: partialRange })
};
let textContent: string;
if (params.response_format === "json") {
textContent = JSON.stringify(output, null, 2);
} else {
const lines = [`# File: ${params.path}`, "",
`Size: ${fileStats.size} bytes | Lines: ${totalLines}`, ""];
if (truncated) {
lines.push(`⚠️ Content truncated to ${params.max_chars} characters`);
}
if (partialRange) {
lines.push(`📄 Showing lines ${partialRange.line_start}-${partialRange.line_end}`);
}
lines.push("", "```", content, "```");
textContent = lines.join('\n');
}
return {
content: [{ type: "text", text: textContent }],
structuredContent: output
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
let errorText: string;
if (error instanceof Error && 'code' in error) {
const code = (error as { code: string }).code;
switch (code) {
case "ENOENT":
errorText = `Error: File not found: ${params.path}`;
break;
case "EACCES":
errorText = `Error: Permission denied to read file: ${params.path}`;
break;
default:
errorText = `Error: Failed to read file: ${errorMessage}`;
}
} else {
errorText = `Error: Failed to read file: ${errorMessage}`;
}
return {
content: [{ type: "text", text: errorText }]
};
}
}
);
}

View File

@ -0,0 +1,236 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { promises as fs } from "fs";
import { lstat } from "fs/promises";
import { join } from "path";
import { DEFAULT_FILE_LIMIT, MAX_DEPTH } from "../constants.js";
import type { SearchResult } from "../types.js";
const SearchFilesInputSchema = z.object({
path: z.string()
.min(1, "Path is required")
.describe("Absolute path to the directory to search in"),
pattern: z.string()
.min(1, "Search pattern is required")
.describe("Glob pattern to match files (e.g., '*.ts', '**/*.json')"),
max_results: z.number()
.int()
.min(1)
.max(DEFAULT_FILE_LIMIT)
.default(50)
.describe("Maximum number of results to return"),
include_content: z.boolean()
.default(false)
.describe("Whether to include file content in results"),
response_format: z.enum(["markdown", "json"])
.default("markdown")
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
}).strict();
type SearchFilesInput = z.infer<typeof SearchFilesInputSchema>;
function matchPattern(filename: string, pattern: string): boolean {
// Simple glob matching - convert glob pattern to regex
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
const regex = new RegExp(`^${regexPattern}$`, 'i');
return regex.test(filename);
}
async function* walkDirectory(
dir: string,
maxDepth: number,
currentDepth: number = 0
): AsyncGenerator<string> {
if (currentDepth >= maxDepth) return;
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
yield fullPath;
if (entry.isDirectory()) {
yield* walkDirectory(fullPath, maxDepth, currentDepth + 1);
}
}
}
export function registerSearchFilesTool(server: McpServer) {
server.registerTool(
"search_files",
{
title: "Search Files",
description: `Search for files in a directory using glob patterns.
This tool recursively searches a directory and returns files matching the specified pattern. It supports basic glob patterns like *.ts, **/*.json, etc.
Args:
- path (string): Absolute path to the directory to search in
- pattern (string): Glob pattern to match files (e.g., '*.ts', '**/*.json', 'src/**/*.ts')
- max_results (number): Maximum number of results to return (default: 50, max: 100)
- include_content (boolean): Whether to include file content in results (default: false)
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns:
For JSON format: Structured data with schema:
{
"search_path": string, // Path that was searched
"pattern": string, // Pattern used
"total_found": number, // Total files matching the pattern
"results_returned": number,// Number of results in this response
"truncated": boolean, // Whether results were truncated
"files": [
{
"name": string, // File name
"path": string, // Absolute path
"isDirectory": boolean,
"size": number, // File size in bytes
"modified": string, // ISO date string
"content": string // Present if include_content is true
}
]
}
Examples:
- Use when: "Find all TypeScript files in the project" -> params with path="/path/to/project", pattern="*.ts"
- Use when: "Find all JSON config files recursively" -> params with path="/path/to/project", pattern="**/*.json"
- Use when: "List all Markdown files with their contents" -> params with path="/path/to/docs", pattern="*.md", include_content=true
- Don't use when: You need to read a specific file (use read_file instead)
- Don't use when: You need to list all files without filtering (use list_directory instead)
Error Handling:
- Returns "Error: Directory not found" if the search path does not exist (ENOENT)
- Returns "Error: Path is not a directory" if the path is a file
- Returns "Error: Permission denied" if directories cannot be accessed (EACCES)`,
inputSchema: SearchFilesInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
},
async (params: SearchFilesInput) => {
try {
// Check if path exists and is a directory
const pathStats = await lstat(params.path);
if (!pathStats.isDirectory()) {
return {
content: [{
type: "text",
text: `Error: Path is not a directory: ${params.path}`
}]
};
}
const results: SearchResult[] = [];
let totalFound = 0;
// Search directory
for await (const filePath of walkDirectory(params.path, MAX_DEPTH)) {
const filename = filePath.split(/[/\\]/).pop() || filePath;
if (matchPattern(filename, params.pattern)) {
totalFound++;
if (results.length < params.max_results) {
const fileStats = await lstat(filePath);
const result: SearchResult = {
name: filename,
path: filePath,
isDirectory: fileStats.isDirectory(),
size: fileStats.size,
modified: fileStats.mtime.toISOString()
};
if (params.include_content && !fileStats.isDirectory()) {
try {
const contentBuffer = await fs.readFile(filePath);
result.content = contentBuffer.toString('utf-8');
} catch {
result.content = '[Unable to read file content]';
}
}
results.push(result);
}
}
}
const output = {
search_path: params.path,
pattern: params.pattern,
total_found: totalFound,
results_returned: results.length,
truncated: totalFound > params.max_results,
files: results
};
let textContent: string;
if (params.response_format === "json") {
textContent = JSON.stringify(output, null, 2);
} else {
const lines: string[] = [
`# Search Results: ${params.pattern}`,
`Search path: ${params.path}`,
`Found ${totalFound} file(s), showing ${results.length}`,
""
];
if (output.truncated) {
lines.push(`⚠️ Results truncated. Use max_results=${params.max_results} to see more.`);
lines.push("");
}
for (const file of results) {
const type = file.isDirectory ? "📁" : "📄";
lines.push(`${type} ${file.path}`);
if (params.include_content && file.content) {
lines.push("```");
const preview = file.content.slice(0, 500);
lines.push(file.content.length > 500 ? preview + "..." : preview);
lines.push("```");
}
lines.push("");
}
textContent = lines.join('\n');
}
return {
content: [{ type: "text", text: textContent }],
structuredContent: output
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
let errorText: string;
if (error instanceof Error && 'code' in error) {
const code = (error as { code: string }).code;
switch (code) {
case "ENOENT":
errorText = `Error: Directory not found: ${params.path}`;
break;
case "EACCES":
errorText = `Error: Permission denied to access directory: ${params.path}`;
break;
default:
errorText = `Error: Failed to search files: ${errorMessage}`;
}
} else {
errorText = `Error: Failed to search files: ${errorMessage}`;
}
return {
content: [{ type: "text", text: errorText }]
};
}
}
);
}

View File

@ -0,0 +1,35 @@
/**
* Type definitions for the FileSystem MCP Server.
*/
export interface FileInfo {
name: string;
path: string;
isDirectory: boolean;
size: number;
modified: string;
}
export interface DirectoryEntry {
name: string;
path: string;
isDirectory: boolean;
size: number;
modified: string;
}
export interface SearchResult {
name: string;
path: string;
isDirectory: boolean;
size: number;
modified: string;
matchLine?: number;
matchContent?: string;
content?: string;
}
export enum ResponseFormat {
MARKDOWN = "markdown",
JSON = "json"
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}