mirror of
https://github.com/open-webui/openapi-servers
synced 2025-06-26 18:17:04 +00:00
init
This commit is contained in:
245
servers/filesystem/main.py
Normal file
245
servers/filesystem/main.py
Normal file
@@ -0,0 +1,245 @@
|
||||
from fastapi import FastAPI, HTTPException, Body
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from pydantic import BaseModel, Field
|
||||
import os
|
||||
import pathlib
|
||||
import asyncio
|
||||
from typing import List, Optional, Literal
|
||||
import difflib
|
||||
|
||||
app = FastAPI(
|
||||
title="Secure Filesystem API",
|
||||
version="0.2.0",
|
||||
description="A secure file manipulation server for reading, editing, writing, listing, and searching files with access restrictions.",
|
||||
)
|
||||
|
||||
# Constants
|
||||
ALLOWED_DIRECTORIES = [
|
||||
str(pathlib.Path(os.path.expanduser("~/mydir")).resolve())
|
||||
] # 👈 Replace with your paths
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Utility functions
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def normalize_path(requested_path: str) -> pathlib.Path:
|
||||
requested = pathlib.Path(os.path.expanduser(requested_path)).resolve()
|
||||
for allowed in ALLOWED_DIRECTORIES:
|
||||
if str(requested).startswith(allowed):
|
||||
return requested
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Access denied: {requested} is outside allowed directories.",
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Pydantic Schemas
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ReadFileRequest(BaseModel):
|
||||
path: str = Field(..., description="Path to the file to read")
|
||||
|
||||
|
||||
class WriteFileRequest(BaseModel):
|
||||
path: str = Field(
|
||||
..., description="Path to write to. Existing file will be overwritten."
|
||||
)
|
||||
content: str = Field(..., description="UTF-8 encoded text content to write.")
|
||||
|
||||
|
||||
class EditOperation(BaseModel):
|
||||
oldText: str = Field(
|
||||
..., description="Text to find and replace (exact match required)"
|
||||
)
|
||||
newText: str = Field(..., description="Replacement text")
|
||||
|
||||
|
||||
class EditFileRequest(BaseModel):
|
||||
path: str = Field(..., description="Path to the file to edit.")
|
||||
edits: List[EditOperation] = Field(..., description="List of edits to apply.")
|
||||
dryRun: bool = Field(
|
||||
False, description="If true, only return diff without modifying file."
|
||||
)
|
||||
|
||||
|
||||
class CreateDirectoryRequest(BaseModel):
|
||||
path: str = Field(
|
||||
...,
|
||||
description="Directory path to create. Intermediate dirs are created automatically.",
|
||||
)
|
||||
|
||||
|
||||
class ListDirectoryRequest(BaseModel):
|
||||
path: str = Field(..., description="Directory path to list contents for.")
|
||||
|
||||
|
||||
class DirectoryTreeRequest(BaseModel):
|
||||
path: str = Field(
|
||||
..., description="Directory path for which to return recursive tree."
|
||||
)
|
||||
|
||||
|
||||
class SearchFilesRequest(BaseModel):
|
||||
path: str = Field(..., description="Base directory to search in.")
|
||||
pattern: str = Field(
|
||||
..., description="Filename pattern (case-insensitive substring match)."
|
||||
)
|
||||
excludePatterns: Optional[List[str]] = Field(
|
||||
default=[], description="Patterns to exclude."
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Routes
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@app.post("/read_file", response_class=PlainTextResponse, summary="Read a file")
|
||||
async def read_file(data: ReadFileRequest = Body(...)):
|
||||
"""
|
||||
Read the entire contents of a file.
|
||||
"""
|
||||
path = normalize_path(data.path)
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/write_file", response_class=PlainTextResponse, summary="Write to a file")
|
||||
async def write_file(data: WriteFileRequest = Body(...)):
|
||||
"""
|
||||
Write content to a file, overwriting if it exists.
|
||||
"""
|
||||
path = normalize_path(data.path)
|
||||
try:
|
||||
path.write_text(data.content, encoding="utf-8")
|
||||
return f"Successfully wrote to {data.path}"
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@app.post(
|
||||
"/edit_file", response_class=PlainTextResponse, summary="Edit a file with diff"
|
||||
)
|
||||
async def edit_file(data: EditFileRequest = Body(...)):
|
||||
"""
|
||||
Apply a list of edits to a text file. Support dry-run to get unified diff.
|
||||
"""
|
||||
path = normalize_path(data.path)
|
||||
original = path.read_text(encoding="utf-8")
|
||||
modified = original
|
||||
|
||||
for edit in data.edits:
|
||||
if edit.oldText not in modified:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"oldText not found in content: {edit.oldText[:50]}",
|
||||
)
|
||||
modified = modified.replace(edit.oldText, edit.newText, 1)
|
||||
|
||||
if data.dryRun:
|
||||
diff = difflib.unified_diff(
|
||||
original.splitlines(keepends=True),
|
||||
modified.splitlines(keepends=True),
|
||||
fromfile="original",
|
||||
tofile="modified",
|
||||
)
|
||||
return "".join(diff)
|
||||
|
||||
path.write_text(modified, encoding="utf-8")
|
||||
return f"Successfully edited file {data.path}"
|
||||
|
||||
|
||||
@app.post(
|
||||
"/create_directory", response_class=PlainTextResponse, summary="Create a directory"
|
||||
)
|
||||
async def create_directory(data: CreateDirectoryRequest = Body(...)):
|
||||
"""
|
||||
Create a new directory recursively.
|
||||
"""
|
||||
dir_path = normalize_path(data.path)
|
||||
try:
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
return f"Successfully created directory {data.path}"
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@app.post(
|
||||
"/list_directory", response_class=PlainTextResponse, summary="List a directory"
|
||||
)
|
||||
async def list_directory(data: ListDirectoryRequest = Body(...)):
|
||||
"""
|
||||
List contents of a directory.
|
||||
"""
|
||||
dir_path = normalize_path(data.path)
|
||||
if not dir_path.is_dir():
|
||||
raise HTTPException(status_code=400, detail="Provided path is not a directory")
|
||||
|
||||
listing = []
|
||||
for entry in dir_path.iterdir():
|
||||
prefix = "[DIR]" if entry.is_dir() else "[FILE]"
|
||||
listing.append(f"{prefix} {entry.name}")
|
||||
|
||||
return "\n".join(listing)
|
||||
|
||||
|
||||
@app.post("/directory_tree", summary="Recursive directory tree")
|
||||
async def directory_tree(data: DirectoryTreeRequest = Body(...)):
|
||||
"""
|
||||
Recursively return a tree structure of a directory.
|
||||
"""
|
||||
base_path = normalize_path(data.path)
|
||||
|
||||
def build_tree(current: pathlib.Path):
|
||||
entries = []
|
||||
for item in current.iterdir():
|
||||
entry = {
|
||||
"name": item.name,
|
||||
"type": "directory" if item.is_dir() else "file",
|
||||
}
|
||||
if item.is_dir():
|
||||
entry["children"] = build_tree(item)
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
return build_tree(base_path)
|
||||
|
||||
|
||||
@app.post("/search_files", summary="Search for files")
|
||||
async def search_files(data: SearchFilesRequest = Body(...)):
|
||||
"""
|
||||
Search files and directories matching a pattern.
|
||||
"""
|
||||
base_path = normalize_path(data.path)
|
||||
results = []
|
||||
|
||||
for root, dirs, files in os.walk(base_path):
|
||||
root_path = pathlib.Path(root)
|
||||
# Apply exclusion patterns
|
||||
excluded = False
|
||||
for pattern in data.excludePatterns:
|
||||
if pathlib.Path(root).match(pattern):
|
||||
excluded = True
|
||||
break
|
||||
if excluded:
|
||||
continue
|
||||
for item in files + dirs:
|
||||
if data.pattern.lower() in item.lower():
|
||||
result_path = root_path / item
|
||||
if any(str(result_path).startswith(alt) for alt in ALLOWED_DIRECTORIES):
|
||||
results.append(str(result_path))
|
||||
|
||||
return {"matches": results or ["No matches found"]}
|
||||
|
||||
|
||||
@app.get("/list_allowed_directories", summary="List access-permitted directories")
|
||||
async def list_allowed_directories():
|
||||
"""
|
||||
Show all directories this server can access.
|
||||
"""
|
||||
return {"allowed_directories": ALLOWED_DIRECTORIES}
|
||||
0
servers/mcp-proxy/main.py
Normal file
0
servers/mcp-proxy/main.py
Normal file
161
servers/time/main.py
Normal file
161
servers/time/main.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from fastapi import FastAPI, HTTPException, Body
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Literal
|
||||
import pytz
|
||||
from dateutil import parser as dateutil_parser
|
||||
|
||||
app = FastAPI(
|
||||
title="Secure Time Utilities API",
|
||||
version="1.0.0",
|
||||
description="Provides secure UTC/local time retrieval, formatting, timezone conversion, and comparison.",
|
||||
)
|
||||
|
||||
# -------------------------------
|
||||
# Pydantic models
|
||||
# -------------------------------
|
||||
|
||||
|
||||
class FormatTimeInput(BaseModel):
|
||||
format: str = Field(
|
||||
"%Y-%m-%d %H:%M:%S", description="Python strftime format string"
|
||||
)
|
||||
timezone: str = Field(
|
||||
"UTC", description="IANA timezone name (e.g., UTC, America/New_York)"
|
||||
)
|
||||
|
||||
|
||||
class ConvertTimeInput(BaseModel):
|
||||
timestamp: str = Field(
|
||||
..., description="ISO 8601 formatted time string (e.g., 2024-01-01T12:00:00Z)"
|
||||
)
|
||||
from_tz: str = Field(
|
||||
..., description="Original IANA time zone of input (e.g. UTC or Europe/Berlin)"
|
||||
)
|
||||
to_tz: str = Field(..., description="Target IANA time zone to convert to")
|
||||
|
||||
|
||||
class ElapsedTimeInput(BaseModel):
|
||||
start: str = Field(..., description="Start timestamp in ISO 8601 format")
|
||||
end: str = Field(..., description="End timestamp in ISO 8601 format")
|
||||
units: Literal["seconds", "minutes", "hours", "days"] = Field(
|
||||
"seconds", description="Unit for elapsed time"
|
||||
)
|
||||
|
||||
|
||||
class ParseTimestampInput(BaseModel):
|
||||
timestamp: str = Field(
|
||||
..., description="Flexible input timestamp string (e.g., 2024-06-01 12:00 PM)"
|
||||
)
|
||||
timezone: str = Field(
|
||||
"UTC", description="Assumed timezone if none is specified in input"
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------
|
||||
# Routes
|
||||
# -------------------------------
|
||||
|
||||
|
||||
@app.get("/get_current_utc_time", summary="Current UTC time")
|
||||
def get_current_utc():
|
||||
"""
|
||||
Returns the current time in UTC in ISO format.
|
||||
"""
|
||||
return {"utc": datetime.utcnow().replace(tzinfo=timezone.utc).isoformat()}
|
||||
|
||||
|
||||
@app.get("/get_current_local_time", summary="Current Local Time")
|
||||
def get_current_local():
|
||||
"""
|
||||
Returns the current time in local timezone in ISO format.
|
||||
"""
|
||||
return {"local_time": datetime.now().isoformat()}
|
||||
|
||||
|
||||
@app.post("/format_time", summary="Format current time")
|
||||
def format_current_time(data: FormatTimeInput):
|
||||
"""
|
||||
Return the current time formatted for a specific timezone and format.
|
||||
"""
|
||||
try:
|
||||
tz = pytz.timezone(data.timezone)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Invalid timezone: {data.timezone}"
|
||||
)
|
||||
now = datetime.now(tz)
|
||||
try:
|
||||
return {"formatted_time": now.strftime(data.format)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid format string: {e}")
|
||||
|
||||
|
||||
@app.post("/convert_time", summary="Convert between timezones")
|
||||
def convert_time(data: ConvertTimeInput):
|
||||
"""
|
||||
Convert a timestamp from one timezone to another.
|
||||
"""
|
||||
try:
|
||||
from_zone = pytz.timezone(data.from_tz)
|
||||
to_zone = pytz.timezone(data.to_tz)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid timezone: {e}")
|
||||
|
||||
try:
|
||||
dt = dateutil_parser.parse(data.timestamp)
|
||||
if dt.tzinfo is None:
|
||||
dt = from_zone.localize(dt)
|
||||
else:
|
||||
dt = dt.astimezone(from_zone)
|
||||
converted = dt.astimezone(to_zone)
|
||||
return {"converted_time": converted.isoformat()}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid timestamp: {e}")
|
||||
|
||||
|
||||
@app.post("/elapsed_time", summary="Time elapsed between timestamps")
|
||||
def elapsed_time(data: ElapsedTimeInput):
|
||||
"""
|
||||
Calculate the difference between two timestamps in chosen units.
|
||||
"""
|
||||
try:
|
||||
start_dt = dateutil_parser.parse(data.start)
|
||||
end_dt = dateutil_parser.parse(data.end)
|
||||
delta = end_dt - start_dt
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid timestamps: {e}")
|
||||
|
||||
seconds = delta.total_seconds()
|
||||
result = {
|
||||
"seconds": seconds,
|
||||
"minutes": seconds / 60,
|
||||
"hours": seconds / 3600,
|
||||
"days": seconds / 86400,
|
||||
}
|
||||
|
||||
return {"elapsed": result[data.units], "unit": data.units}
|
||||
|
||||
|
||||
@app.post("/parse_timestamp", summary="Parse and normalize timestamps")
|
||||
def parse_timestamp(data: ParseTimestampInput):
|
||||
"""
|
||||
Parse human-friendly input timestamp and return standardized UTC ISO time.
|
||||
"""
|
||||
try:
|
||||
tz = pytz.timezone(data.timezone)
|
||||
dt = dateutil_parser.parse(data.timestamp)
|
||||
if dt.tzinfo is None:
|
||||
dt = tz.localize(dt)
|
||||
dt_utc = dt.astimezone(pytz.utc)
|
||||
return {"utc": dt_utc.isoformat()}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Could not parse: {e}")
|
||||
|
||||
|
||||
@app.get("/list_time_zones", summary="All valid time zones")
|
||||
def list_time_zones():
|
||||
"""
|
||||
Return a list of all valid IANA time zones.
|
||||
"""
|
||||
return pytz.all_timezones
|
||||
Reference in New Issue
Block a user