# coding=utf-8 """ @Time :2026/6/30 @Author :tao.chen """ from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from files_mcp.tools.files import ( copy as copy_file, create_file, delete_file, list_dir, move as move_file, read_file, search, stat, update_file, ) from files_mcp.tools.requests import ( CopyRequest, CreateFileRequest, DeleteFileRequest, ListDirRequest, MoveRequest, ReadFileRequest, SearchRequest, StatRequest, UpdateFileRequest, ) app = FastAPI( title="Files MCP", version="0.0.1", description="Files MCP Server — sandboxed file-system CRUD over fastapi-mcp", ) # --- Exception handlers --- # KeyError -> 404 (path not found) # ValueError -> 400 (bad input: escape, non-utf-8, too large, wrong type) # FileExistsError -> 409 (create/move/copy hit existing dst without overwrite) # # Only the third handler is new here; the first two mirror the # spark_executor.server convention. They live on this app only, so the # spark_executor handlers are unaffected. @app.exception_handler(KeyError) async def _keyerror_handler(_request: Request, exc: KeyError) -> JSONResponse: return JSONResponse(status_code=404, content={"detail": str(exc)}) @app.exception_handler(ValueError) async def _valueerror_handler(_request: Request, exc: ValueError) -> JSONResponse: return JSONResponse(status_code=400, content={"detail": str(exc)}) @app.exception_handler(FileExistsError) async def _fileexists_handler(_request: Request, exc: FileExistsError) -> JSONResponse: return JSONResponse(status_code=409, content={"detail": str(exc)}) # --- Routes --- @app.post( "/create_file", operation_id="create_file", summary="Create a new text file inside the sandbox", description=( "Writes `content` to `path` (resolved against FILES_MCP_ROOT). " "Atomic write via tempfile + os.replace. Default fails with 409 " "if the file already exists; pass overwrite=true to replace. " "Content is capped at 1 MB. utf-8 only." ), ) def _create_file(req: CreateFileRequest): return create_file(path=req.path, content=req.content, overwrite=req.overwrite) @app.post( "/read_file", operation_id="read_file", summary="Read a text file inside the sandbox", description=( "Returns the full utf-8 content. Files larger than 1 MB and " "non-utf-8 files are rejected with 400. Directories are rejected " "with 400; use list_dir instead." ), ) def _read_file(req: ReadFileRequest): return read_file(path=req.path) @app.post( "/update_file", operation_id="update_file", summary="Overwrite an existing text file inside the sandbox", description=( "Replaces the file in full. 404 if the path does not exist. " "Atomic write, 1 MB cap, utf-8 only." ), ) def _update_file(req: UpdateFileRequest): return update_file(path=req.path, content=req.content) @app.post( "/delete_file", operation_id="delete_file", summary="Delete a file or empty directory", description=( "404 if the path does not exist. To delete a non-empty directory, " "pass recursive=true." ), ) def _delete_file(req: DeleteFileRequest): return delete_file(path=req.path, recursive=req.recursive) @app.post( "/list_dir", operation_id="list_dir", summary="List entries in a directory", description=( "Returns a list of FileEntry records. recursive=false lists only " "the directory's direct children; recursive=true descends up to " "max_depth levels (0 = the directory itself, 1 = one level deep)." ), ) def _list_dir(req: ListDirRequest): return list_dir(path=req.path, recursive=req.recursive, max_depth=req.max_depth) @app.post( "/stat", operation_id="stat", summary="Get a single FileEntry (path, type, size, mtime, mode)", description="404 if the path does not exist.", ) def _stat(req: StatRequest): return stat(path=req.path) @app.post( "/search", operation_id="search", summary="Find paths matching a glob pattern", description=( "Returns the absolute paths of entries inside `path` whose name " "matches the glob `pattern`. recursive=true descends into " "subdirectories; recursive=false matches the directory's direct " "children only." ), ) def _search(req: SearchRequest): return search(path=req.path, pattern=req.pattern, recursive=req.recursive) @app.post( "/move", operation_id="move_file", summary="Rename or move a file or directory", description=( "404 if src does not exist. 409 if dst exists and overwrite=false. " "Cross-filesystem moves are not atomic." ), ) def _move(req: MoveRequest): return move_file(src=req.src, dst=req.dst, overwrite=req.overwrite) @app.post( "/copy", operation_id="copy_file", summary="Copy a file or directory", description=( "404 if src does not exist. 409 if dst exists and overwrite=false. " "For files, metadata is preserved (shutil.copy2). For directories, " "the entire tree is copied (shutil.copytree)." ), ) def _copy(req: CopyRequest): return copy_file(src=req.src, dst=req.dst, overwrite=req.overwrite)