MCP server source
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Filesystem MCP Server — Safe file operations for Open WebUI."""
|
||||
from fastmcp import FastMCP
|
||||
import os, glob, fnmatch
|
||||
|
||||
ALLOWED_ROOTS = ['/root/projects', '/root/docker']
|
||||
mcp = FastMCP('Filesystem', version='1.0.0')
|
||||
|
||||
def _safe_path(path: str) -> str:
|
||||
"""Resolve and validate path is within allowed roots."""
|
||||
real = os.path.realpath(path)
|
||||
for root in ALLOWED_ROOTS:
|
||||
if real.startswith(os.path.realpath(root) + os.sep) or real == os.path.realpath(root):
|
||||
return real
|
||||
raise ValueError(f'Path outside allowed roots: {path}. Allowed: {ALLOWED_ROOTS}')
|
||||
|
||||
@mcp.tool(description="List files and folders in a directory")
|
||||
async def list_directory(path: str = '/root/projects') -> str:
|
||||
"""List contents of a directory."""
|
||||
real = _safe_path(path)
|
||||
if not os.path.isdir(real):
|
||||
return f'Not a directory: {path}'
|
||||
items = os.listdir(real)
|
||||
result = [f'Directory: {real}']
|
||||
for item in sorted(items):
|
||||
full = os.path.join(real, item)
|
||||
typ = 'DIR' if os.path.isdir(full) else 'FILE'
|
||||
size = os.path.getsize(full) if os.path.isfile(full) else 0
|
||||
result.append(f' [{typ}] {item} ({size:,} bytes)')
|
||||
return '\n'.join(result[:100])
|
||||
|
||||
@mcp.tool(description="Read a file's contents, optionally limited to N lines")
|
||||
async def read_file(path: str, max_lines: int = 100) -> str:
|
||||
"""Read file contents."""
|
||||
real = _safe_path(path)
|
||||
if not os.path.isfile(real):
|
||||
return f'Not a file: {path}'
|
||||
with open(real) as f:
|
||||
lines = f.readlines()
|
||||
if max_lines and len(lines) > max_lines:
|
||||
return ''.join(lines[:max_lines]) + f'\n... ({len(lines)} total lines, showing first {max_lines})'
|
||||
return ''.join(lines)
|
||||
|
||||
@mcp.tool(description="Write content to a file (overwrites existing)")
|
||||
async def write_file(path: str, content: str) -> str:
|
||||
"""Write content to a file."""
|
||||
real = _safe_path(path)
|
||||
os.makedirs(os.path.dirname(real), exist_ok=True)
|
||||
with open(real, 'w') as f:
|
||||
f.write(content)
|
||||
return f'Written {len(content):,} bytes to {real}'
|
||||
|
||||
@mcp.tool(description="Search for files by name pattern (glob)")
|
||||
async def search_files(pattern: str, directory: str = '/root/projects') -> str:
|
||||
"""Search for files matching a glob pattern."""
|
||||
real = _safe_path(directory)
|
||||
if not os.path.isdir(real):
|
||||
return f'Not a directory: {directory}'
|
||||
matches = []
|
||||
for root, dirs, files in os.walk(real):
|
||||
for name in files + dirs:
|
||||
if fnmatch.fnmatch(name, f'*{pattern}*'):
|
||||
matches.append(os.path.join(root, name))
|
||||
if not matches:
|
||||
return f'No files matching "{pattern}" in {real}'
|
||||
return f'Found {len(matches)} matches:\n' + '\n'.join(matches[:50])
|
||||
|
||||
@mcp.tool(description="Create a new directory")
|
||||
async def create_directory(path: str) -> str:
|
||||
"""Create a directory and parents."""
|
||||
real = _safe_path(path)
|
||||
os.makedirs(real, exist_ok=True)
|
||||
return f'Created directory: {real}'
|
||||
|
||||
if __name__ == '__main__':
|
||||
mcp.run(transport='http', host='0.0.0.0', port=8900, path='/mcp')
|
||||
Reference in New Issue
Block a user