mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
* refactor: rewrite filesystem MCP server with new tool set - Replace existing filesystem MCP with modular architecture - Implement 6 new tools: glob, ls, grep, read, write, delete - Add comprehensive TypeScript types and Zod schemas - Maintain security with path validation and allowed directories - Improve error handling and user feedback - Add result limits for performance (100 files/matches max) - Format output with clear, helpful messages - Keep backward compatibility with existing import patterns BREAKING CHANGE: Tools renamed from snake_case to lowercase - read_file → read - write_file → write - list_directory → ls - search_files → glob - New tools: grep, delete - Removed: edit_file, create_directory, directory_tree, move_file, get_file_info * 🐛 fix: remove filesystem allowed directories restriction * 🐛 fix: relax binary detection for text files * ✨ feat: add edit tool with fuzzy matching to filesystem MCP server - Add edit tool with 9 fallback replacers from opencode for robust string replacement (SimpleReplacer, LineTrimmedReplacer, BlockAnchorReplacer, WhitespaceNormalizedReplacer, etc.) - Add Levenshtein distance algorithm for similarity matching - Improve descriptions for all tools (read, write, glob, grep, ls, delete) following opencode patterns for better LLM guidance - Register edit tool in server and export from tools index * ♻️ refactor: replace allowedDirectories with baseDir in filesystem MCP server - Change server to use single baseDir (from WORKSPACE_ROOT env or userData/workspace default) - Remove list_allowed_directories tool as restriction mechanism is removed - Add ripgrep integration for faster grep searches with JS fallback - Simplify validatePath() by removing allowlist checks - Display paths relative to baseDir in tool outputs * 📝 docs: standardize filesystem MCP server tool descriptions - Unify description format to bullet-point style across all tools - Add absolute path requirement to ls, glob, grep schemas and descriptions - Update glob and grep to output absolute paths instead of relative paths - Add missing error case documentation for edit tool (old_string === new_string) - Standardize optional path parameter descriptions * ♻️ refactor: use ripgrep for glob tool and extract shared utilities - Extract shared ripgrep utilities (runRipgrep, getRipgrepAddonPath) to types.ts - Rewrite glob tool to use `rg --files --glob` for reliable file matching - Update grep tool to import shared ripgrep utilities * 🐛 fix: handle ripgrep exit code 2 with valid results in glob tool - Process ripgrep stdout when content exists, regardless of exit code - Exit code 2 can indicate partial errors while still returning valid results - Remove fallback directory listing (had buggy regex for root-level files) - Update tool description to clarify patterns without "/" match at any depth * 🔥 chore: remove filesystem.ts.backup file Remove unnecessary backup file from mcpServers directory * 🐛 fix: use correct default workspace path in filesystem MCP server Change default baseDir from userData/workspace to userData/Data/Workspace to match the app's data storage convention (Data/Files, Data/Notes, etc.) Addresses PR #11937 review feedback. * 🐛 fix: pass WORKSPACE_ROOT to FileSystemServer constructor The envs object passed to createInMemoryMCPServer was not being used for the filesystem server. Now WORKSPACE_ROOT is passed as a constructor parameter, following the same pattern as other MCP servers. * \feat: add link to documentation for MCP server configuration requirement Wrap the configuration requirement tag in a link to the documentation for better user guidance on MCP server settings. --------- Co-authored-by: kangfenmao <kangfenmao@qq.com>
84 lines
2.4 KiB
TypeScript
84 lines
2.4 KiB
TypeScript
import fs from 'fs/promises'
|
|
import path from 'path'
|
|
import * as z from 'zod'
|
|
|
|
import { logger, validatePath } from '../types'
|
|
|
|
// Schema definition
|
|
export const WriteToolSchema = z.object({
|
|
file_path: z.string().describe('The path to the file to write'),
|
|
content: z.string().describe('The content to write to the file')
|
|
})
|
|
|
|
// Tool definition with detailed description
|
|
export const writeToolDefinition = {
|
|
name: 'write',
|
|
description: `Writes a file to the local filesystem.
|
|
|
|
- This tool will overwrite the existing file if one exists at the path
|
|
- You MUST use the read tool first to understand what you're overwriting
|
|
- ALWAYS prefer using the 'edit' tool for existing files
|
|
- NEVER proactively create documentation files unless explicitly requested
|
|
- Parent directories will be created automatically if they don't exist
|
|
- The file_path must be an absolute path, not a relative path`,
|
|
inputSchema: z.toJSONSchema(WriteToolSchema)
|
|
}
|
|
|
|
// Handler implementation
|
|
export async function handleWriteTool(args: unknown, baseDir: string) {
|
|
const parsed = WriteToolSchema.safeParse(args)
|
|
if (!parsed.success) {
|
|
throw new Error(`Invalid arguments for write: ${parsed.error}`)
|
|
}
|
|
|
|
const filePath = parsed.data.file_path
|
|
const validPath = await validatePath(filePath, baseDir)
|
|
|
|
// Create parent directory if it doesn't exist
|
|
const parentDir = path.dirname(validPath)
|
|
try {
|
|
await fs.mkdir(parentDir, { recursive: true })
|
|
} catch (error: any) {
|
|
if (error.code !== 'EEXIST') {
|
|
throw new Error(`Failed to create parent directory: ${error.message}`)
|
|
}
|
|
}
|
|
|
|
// Check if file exists (for logging)
|
|
let isOverwrite = false
|
|
try {
|
|
await fs.stat(validPath)
|
|
isOverwrite = true
|
|
} catch {
|
|
// File doesn't exist, that's fine
|
|
}
|
|
|
|
// Write the file
|
|
try {
|
|
await fs.writeFile(validPath, parsed.data.content, 'utf-8')
|
|
} catch (error: any) {
|
|
throw new Error(`Failed to write file: ${error.message}`)
|
|
}
|
|
|
|
// Log the operation
|
|
logger.info('File written', {
|
|
path: validPath,
|
|
overwrite: isOverwrite,
|
|
size: parsed.data.content.length
|
|
})
|
|
|
|
// Format output
|
|
const relativePath = path.relative(baseDir, validPath)
|
|
const action = isOverwrite ? 'Updated' : 'Created'
|
|
const lines = parsed.data.content.split('\n').length
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: `${action} file: ${relativePath}\n` + `Size: ${parsed.data.content.length} bytes\n` + `Lines: ${lines}`
|
|
}
|
|
]
|
|
}
|
|
}
|