Dylan Mann-Krzisnik commited on
Commit
86ef064
·
1 Parent(s): 9d0e6df

Add /upload and /outputs endpoints for file persistence; add / health check

Browse files

- Wrap FastMCP server in FastAPI app to add extra HTTP routes
- POST /upload: upload local files to server's /data/inputs directory
- GET /outputs/{name}: download files from server's /data/outputs directory
- GET /: health check endpoint for HF Spaces liveness probes
- Mount MCP sub-app at /mcp with correct path handling
- Add python-multipart dependency for file upload support

Files changed (2) hide show
  1. requirements.txt +1 -0
  2. src/GLUE_Agent_mcp.py +69 -8
requirements.txt CHANGED
@@ -3,6 +3,7 @@ fastmcp==2.14.5
3
  uvicorn==0.40.0
4
  fastapi
5
  starlette==0.52.1
 
6
 
7
  # Bioinformatics core
8
  anndata==0.11.4
 
3
  uvicorn==0.40.0
4
  fastapi
5
  starlette==0.52.1
6
+ python-multipart
7
 
8
  # Bioinformatics core
9
  anndata==0.11.4
src/GLUE_Agent_mcp.py CHANGED
@@ -13,28 +13,89 @@ This MCP Server contains tools extracted from the following tutorial files:
13
  - glue_train_model: Train GLUE model for multi-omics integration
14
  - glue_check_integration_consistency: Evaluate integration quality with consistency scores
15
  - glue_generate_embeddings: Generate cell and feature embeddings from trained GLUE model
 
 
 
 
 
 
 
16
  """
17
 
18
  import os
 
19
 
 
 
20
  from fastmcp import FastMCP
21
 
22
- # Import statements (alphabetical order)
23
  from tools.preprocessing import preprocessing_mcp
24
  from tools.training import training_mcp
25
 
26
- # Server definition and mounting
 
 
 
 
 
 
 
27
  mcp = FastMCP(name="GLUE_Agent")
28
  mcp.mount(preprocessing_mcp)
29
  mcp.mount(training_mcp)
30
 
31
- # ASGI app for uvicorn (used when deployed as a remote HTTP server)
32
- app = mcp.http_app(path="/mcp")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  if __name__ == "__main__":
35
- mcp.run(
36
- transport="http",
 
 
37
  host="0.0.0.0",
38
  port=int(os.getenv("PORT", 7860)),
39
- path="/mcp",
40
- )
 
13
  - glue_train_model: Train GLUE model for multi-omics integration
14
  - glue_check_integration_consistency: Evaluate integration quality with consistency scores
15
  - glue_generate_embeddings: Generate cell and feature embeddings from trained GLUE model
16
+
17
+ Remote endpoint layout
18
+ ----------------------
19
+ GET / — health check (HF Spaces liveness probe)
20
+ POST /upload — upload a local file to /data/inputs on the server
21
+ GET /outputs/... — download a file previously written to /data/outputs
22
+ /mcp — Streamable HTTP MCP endpoint (used by Claude Code)
23
  """
24
 
25
  import os
26
+ from pathlib import Path
27
 
28
+ from fastapi import FastAPI, HTTPException, UploadFile
29
+ from fastapi.responses import FileResponse
30
  from fastmcp import FastMCP
31
 
 
32
  from tools.preprocessing import preprocessing_mcp
33
  from tools.training import training_mcp
34
 
35
+ # These env vars are set in the Dockerfile; both tool modules honour them too.
36
+ INPUT_DIR = Path(os.getenv("PREPROCESSING_INPUT_DIR", "/data/inputs"))
37
+ OUTPUT_DIR = Path(os.getenv("PREPROCESSING_OUTPUT_DIR", "/data/outputs"))
38
+
39
+ INPUT_DIR.mkdir(parents=True, exist_ok=True)
40
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
41
+
42
+ # --- MCP server -----------------------------------------------------------
43
  mcp = FastMCP(name="GLUE_Agent")
44
  mcp.mount(preprocessing_mcp)
45
  mcp.mount(training_mcp)
46
 
47
+ # --- Outer FastAPI app ----------------------------------------------------
48
+ app = FastAPI(title="GLUE_Agent MCP Server")
49
+
50
+
51
+ @app.get("/")
52
+ async def health_check():
53
+ """Liveness probe for HF Spaces; also confirms the MCP endpoint path."""
54
+ return {"status": "ok", "mcp_endpoint": "/mcp"}
55
+
56
+
57
+ @app.post("/upload")
58
+ async def upload_file(file: UploadFile):
59
+ """
60
+ Upload a local file to the server's input directory.
61
+
62
+ Usage (from launch_remote_mcp.sh CLAUDE.md template):
63
+ curl -F "file=@/absolute/local/path" ${REMOTE_MCP_URL}/upload
64
+ """
65
+ dest = INPUT_DIR / file.filename
66
+ dest.parent.mkdir(parents=True, exist_ok=True)
67
+ content = await file.read()
68
+ dest.write_bytes(content)
69
+ return {"filename": file.filename, "path": str(dest), "size": len(content)}
70
+
71
+
72
+ @app.get("/outputs/{name:path}")
73
+ async def download_output(name: str):
74
+ """
75
+ Download a file previously written by an MCP tool.
76
+
77
+ Usage (from launch_remote_mcp.sh CLAUDE.md template):
78
+ wget ${REMOTE_MCP_URL}/outputs/<output_filename>
79
+ """
80
+ # Resolve and guard against path-traversal attacks
81
+ file_path = (OUTPUT_DIR / name).resolve()
82
+ if not str(file_path).startswith(str(OUTPUT_DIR.resolve())):
83
+ raise HTTPException(status_code=400, detail="Invalid file path")
84
+ if not file_path.is_file():
85
+ raise HTTPException(status_code=404, detail=f"Output file not found: {name}")
86
+ return FileResponse(str(file_path), filename=file_path.name)
87
+
88
+
89
+ # Mount the MCP ASGI sub-app at /mcp.
90
+ # path="/" because FastAPI strips the "/mcp" prefix before forwarding to the
91
+ # sub-app, so the sub-app must handle requests at its own root.
92
+ app.mount("/mcp", mcp.http_app(path="/"))
93
 
94
  if __name__ == "__main__":
95
+ import uvicorn
96
+
97
+ uvicorn.run(
98
+ app,
99
  host="0.0.0.0",
100
  port=int(os.getenv("PORT", 7860)),
101
+ )