feat(api): add special handling for _mcp_servers file uploads and improve tests (#8847)

* update

* [autofix.ci] apply automated fixes

* refactor: update file query logic and enhance error logging in file deletion

* refactor: enhance error handling and improve code clarity in MCP server list retrieval

* refactor: improve type hinting and code clarity in MCP servers file tests

* fix: handle potential None value for server configuration file in async server list retrieval

* Update test_files.py

* fix: autofix.ci / Update Starter Projects (pull_request)

* chore: Add API key environment variables to CI workflows

* Updated .github/workflows/ci.yml and .github/workflows/python_test.yml to include OPENAI_API_KEY, ANTHROPIC_API_KEY, and other relevant API keys as environment variables for CI jobs.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
Co-authored-by: Carlos Coelho <80289056+carlosrcoelho@users.noreply.github.com>
Co-authored-by: italojohnny <italojohnnydosanjos@gmail.com>
This commit is contained in:
Edwin Jose 2025-07-04 11:20:04 -05:00 • committed by GitHub
commit a88829a9d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 239 additions and 47 deletions

View file

@ -11,7 +11,8 @@ from zoneinfo import ZoneInfo
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from sqlmodel import String, cast, col, select
from loguru import logger
from sqlmodel import col, select
from langflow.api.schemas import UploadFileResponse
from langflow.api.utils import CurrentActiveUser, DbSession
@ -112,35 +113,36 @@ async def upload_user_file(
# Create a new database record for the uploaded file.
try:
# Enforce unique constraint on name
# Name it as filename (1), (2), etc.
# Check if the file name already exists
# Enforce unique constraint on name, except for the special _mcp_servers file
new_filename = file.filename
try:
root_filename, _ = new_filename.rsplit(".", 1)
except ValueError:
root_filename, _ = new_filename, ""
# Check if there are files with the same name
stmt = select(UserFile).where(cast(UserFile.name, String).like(f"{root_filename}%"))
existing_files = await session.exec(stmt)
files = existing_files.all() # Fetch all matching records
# Special handling for the MCP servers config file: always keep the same root filename
if root_filename == MCP_SERVERS_FILE:
# Check if an existing record exists; if so, delete it to replace with the new one
existing_mcp_file = await get_file_by_name(root_filename, current_user, session)
if existing_mcp_file:
await delete_file(existing_mcp_file.id, current_user, session, storage_service)
else:
# For normal files, ensure unique name by appending a count if necessary
stmt = select(UserFile).where(col(UserFile.name).like(f"{root_filename}%"))
existing_files = await session.exec(stmt)
files = existing_files.all() # Fetch all matching records
# If there are files with the same name, append a count to the filename
if files:
counts = []
if files:
counts = []
# Extract the count from the filename
for my_file in files:
match = re.search(r"\((\d+)\)(?=\.\w+$|$)", my_file.name) # Match (number) before extension or at end
if match:
counts.append(int(match.group(1)))
# Extract the count from the filename
for my_file in files:
match = re.search(r"\((\d+)\)(?=\.\w+$|$)", my_file.name)
if match:
counts.append(int(match.group(1)))
# Get the max count and increment by 1
count = max(counts) if counts else 0 # Default to 0 if no matches found
# Split the extension from the filename
root_filename = f"{root_filename} ({count + 1})"
count = max(counts) if counts else 0
root_filename = f"{root_filename} ({count + 1})"
# Compute the file size based on the path
file_size = await storage_service.get_file_size(
@ -462,24 +464,26 @@ async def delete_file(
):
"""Delete a file by its ID."""
try:
# Fetch the file from the DB
file = await fetch_file_object(file_id, current_user, session)
if not file:
# Fetch the file object
file_to_delete = await fetch_file_object(file_id, current_user, session)
if not file_to_delete:
raise HTTPException(status_code=404, detail="File not found")
# Delete the file from the storage service
await storage_service.delete_file(flow_id=str(current_user.id), file_name=file.path)
await storage_service.delete_file(flow_id=str(current_user.id), file_name=file_to_delete.path)
# Delete from the database
await session.delete(file)
await session.delete(file_to_delete)
await session.flush() # Ensures delete is staged
await session.commit() # Commit deletion
except HTTPException:
# Re-raise HTTPException to avoid being caught by the generic exception handler
raise
except Exception as e:
await session.rollback() # Rollback on failure
# Log and return a generic server error
logger.error("Error deleting file %s: %s", file_id, e)
raise HTTPException(status_code=500, detail=f"Error deleting file: {e}") from e
return {"message": "File deleted successfully"}
return {"detail": f"File {file_to_delete.name} deleted successfully"}
@router.delete("")

View file

@ -1,3 +1,4 @@
import contextlib
import json
from io import BytesIO
@ -43,8 +44,22 @@ async def get_server_list(
# Read the server configuration from a file using the files api
server_config_file = await get_file_by_name(MCP_SERVERS_FILE, current_user, session)
# If the file does not exist, create a new one with an empty configuration
if not server_config_file:
# Attempt to download the configuration file content
try:
server_config_bytes = await download_file(
server_config_file.id if server_config_file else None,
current_user,
session,
storage_service=storage_service,
return_content=True,
)
except (FileNotFoundError, HTTPException):
# Storage file missing - DB entry may be stale. Remove it and recreate.
if server_config_file:
with contextlib.suppress(Exception):
await delete_file(server_config_file.id, current_user, session, storage_service)
# Create a fresh empty config
await upload_server_config(
{"mcpServers": {}},
current_user,
@ -52,24 +67,23 @@ async def get_server_list(
storage_service=storage_service,
settings_service=settings_service,
)
# Fetch and download again
server_config_file = await get_file_by_name(MCP_SERVERS_FILE, current_user, session)
if not server_config_file:
raise HTTPException(status_code=500, detail="Failed to create _mcp_servers.json") from None
# Make sure we have it now
if not server_config_file:
raise HTTPException(status_code=500, detail="Server configuration file not found.")
server_config_bytes = await download_file(
server_config_file.id,
current_user,
session,
storage_service=storage_service,
return_content=True,
)
# Download the server configuration file content
server_config = await download_file(
server_config_file.id,
current_user,
session,
storage_service=storage_service,
return_content=True,
)
# Parse the JSON content
# Parse JSON content
try:
servers = json.loads(server_config)
servers = json.loads(server_config_bytes)
except json.JSONDecodeError:
raise HTTPException(status_code=500, detail="Invalid server configuration file format.") from None