Spaces:
Running
Running
Local MCP check
Browse files- README.md +4 -4
- backend/main.py +8 -3
- backend/pyproject.toml +3 -1
- backend/requirements.txt +4 -1
- backend/src/api/main.py +25 -13
- backend/src/api/routes/auth.py +54 -54
- backend/src/mcp/server.py +62 -32
- backend/src/services/auth.py +27 -0
- backend/src/services/config.py +16 -0
- backend/uv.lock +43 -39
- debug_endpoints.py +169 -0
- frontend/package-lock.json +3 -24
- frontend/vite.config.ts +1 -1
- mcp_init_request.json +14 -0
- start-dev.sh +3 -3
- status-dev.sh +1 -1
README.md
CHANGED
|
@@ -114,12 +114,12 @@ Start the HTTP API server:
|
|
| 114 |
cd backend
|
| 115 |
JWT_SECRET_KEY="local-dev-secret-key-123" \
|
| 116 |
VAULT_BASE_PATH="$(pwd)/../data/vaults" \
|
| 117 |
-
.venv/bin/uvicorn src.api.main:app --host 0.0.0.0 --port
|
| 118 |
```
|
| 119 |
|
| 120 |
-
Backend will be available at: `http://localhost:
|
| 121 |
|
| 122 |
-
API docs (Swagger): `http://localhost:
|
| 123 |
|
| 124 |
#### Running MCP Server (STDIO Mode)
|
| 125 |
|
|
@@ -332,7 +332,7 @@ write_note(
|
|
| 332 |
- Verify database is initialized
|
| 333 |
|
| 334 |
**Frontend shows connection errors:**
|
| 335 |
-
- Ensure backend is running on port
|
| 336 |
- Check Vite proxy configuration in `frontend/vite.config.ts`
|
| 337 |
|
| 338 |
**Search returns no results:**
|
|
|
|
| 114 |
cd backend
|
| 115 |
JWT_SECRET_KEY="local-dev-secret-key-123" \
|
| 116 |
VAULT_BASE_PATH="$(pwd)/../data/vaults" \
|
| 117 |
+
.venv/bin/uvicorn src.api.main:app --host 0.0.0.0 --port 8001 --reload
|
| 118 |
```
|
| 119 |
|
| 120 |
+
Backend will be available at: `http://localhost:8001`
|
| 121 |
|
| 122 |
+
API docs (Swagger): `http://localhost:8001/docs`
|
| 123 |
|
| 124 |
#### Running MCP Server (STDIO Mode)
|
| 125 |
|
|
|
|
| 332 |
- Verify database is initialized
|
| 333 |
|
| 334 |
**Frontend shows connection errors:**
|
| 335 |
+
- Ensure backend is running on port 8001
|
| 336 |
- Check Vite proxy configuration in `frontend/vite.config.ts`
|
| 337 |
|
| 338 |
**Search returns no results:**
|
backend/main.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
| 1 |
-
|
| 2 |
-
print("Hello from backend!")
|
| 3 |
|
|
|
|
| 4 |
|
| 5 |
if __name__ == "__main__":
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Entry point for running the FastAPI application."""
|
|
|
|
| 2 |
|
| 3 |
+
import uvicorn
|
| 4 |
|
| 5 |
if __name__ == "__main__":
|
| 6 |
+
uvicorn.run(
|
| 7 |
+
"src.api.main:app",
|
| 8 |
+
host="0.0.0.0",
|
| 9 |
+
port=8000,
|
| 10 |
+
reload=True,
|
| 11 |
+
)
|
backend/pyproject.toml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
[project]
|
| 2 |
-
name = "
|
| 3 |
version = "0.1.0"
|
| 4 |
description = "Add your description here"
|
| 5 |
readme = "README.md"
|
|
@@ -8,7 +8,9 @@ dependencies = [
|
|
| 8 |
"fastapi>=0.121.2",
|
| 9 |
"fastmcp>=2.13.1",
|
| 10 |
"huggingface-hub>=1.1.4",
|
|
|
|
| 11 |
"pyjwt>=2.10.1",
|
|
|
|
| 12 |
"python-frontmatter>=1.1.0",
|
| 13 |
"uvicorn[standard]>=0.38.0",
|
| 14 |
]
|
|
|
|
| 1 |
[project]
|
| 2 |
+
name = "Documentation-MCP"
|
| 3 |
version = "0.1.0"
|
| 4 |
description = "Add your description here"
|
| 5 |
readme = "README.md"
|
|
|
|
| 8 |
"fastapi>=0.121.2",
|
| 9 |
"fastmcp>=2.13.1",
|
| 10 |
"huggingface-hub>=1.1.4",
|
| 11 |
+
"mcp>=1.21.0",
|
| 12 |
"pyjwt>=2.10.1",
|
| 13 |
+
"python-dotenv>=1.0.0",
|
| 14 |
"python-frontmatter>=1.1.0",
|
| 15 |
"uvicorn[standard]>=0.38.0",
|
| 16 |
]
|
backend/requirements.txt
CHANGED
|
@@ -6,4 +6,7 @@ huggingface_hub
|
|
| 6 |
uvicorn
|
| 7 |
httpx
|
| 8 |
pytest
|
| 9 |
-
pytest-asyncio
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
uvicorn
|
| 7 |
httpx
|
| 8 |
pytest
|
| 9 |
+
pytest-asyncio
|
| 10 |
+
python-dotenv
|
| 11 |
+
requests
|
| 12 |
+
sqlite
|
backend/src/api/main.py
CHANGED
|
@@ -10,10 +10,15 @@ from fastapi import FastAPI, HTTPException, Request
|
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
from fastapi.responses import JSONResponse
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
from starlette.responses import Response
|
| 15 |
|
| 16 |
-
from fastmcp.server.
|
|
|
|
| 17 |
|
| 18 |
from .routes import auth, index, notes, search
|
| 19 |
from ..mcp.server import mcp
|
|
@@ -30,7 +35,11 @@ app = FastAPI(
|
|
| 30 |
# CORS middleware
|
| 31 |
app.add_middleware(
|
| 32 |
CORSMiddleware,
|
| 33 |
-
allow_origins=[
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
allow_credentials=True,
|
| 35 |
allow_methods=["*"],
|
| 36 |
allow_headers=["*"],
|
|
@@ -137,34 +146,38 @@ async def health():
|
|
| 137 |
return {"status": "healthy"}
|
| 138 |
|
| 139 |
|
| 140 |
-
# Serve frontend static files with SPA support (must be last to not override API routes)
|
| 141 |
-
from fastapi.responses import FileResponse
|
| 142 |
-
|
| 143 |
frontend_dist = Path(__file__).resolve().parents[3] / "frontend" / "dist"
|
| 144 |
if frontend_dist.exists():
|
| 145 |
# Mount static assets
|
| 146 |
-
app.mount(
|
| 147 |
-
|
|
|
|
|
|
|
| 148 |
# Catch-all route for SPA - serve index.html for all non-API routes
|
| 149 |
@app.get("/{full_path:path}")
|
| 150 |
async def serve_spa(full_path: str):
|
| 151 |
"""Serve the SPA for all non-API routes."""
|
| 152 |
# Don't intercept API or auth routes
|
| 153 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
# Let FastAPI's 404 handler take over
|
| 155 |
raise HTTPException(status_code=404, detail="Not found")
|
| 156 |
-
|
| 157 |
# If the path looks like a file (has extension), try to serve it
|
| 158 |
file_path = frontend_dist / full_path
|
| 159 |
if file_path.is_file():
|
| 160 |
return FileResponse(file_path)
|
| 161 |
# Otherwise serve index.html for SPA routing
|
| 162 |
return FileResponse(frontend_dist / "index.html")
|
| 163 |
-
|
| 164 |
logger.info(f"Serving frontend SPA from: {frontend_dist}")
|
| 165 |
else:
|
| 166 |
logger.warning(f"Frontend dist not found at: {frontend_dist}")
|
| 167 |
-
|
| 168 |
# Fallback health endpoint if no frontend
|
| 169 |
@app.get("/")
|
| 170 |
async def root():
|
|
@@ -173,4 +186,3 @@ else:
|
|
| 173 |
|
| 174 |
|
| 175 |
__all__ = ["app"]
|
| 176 |
-
|
|
|
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
from fastapi.responses import JSONResponse
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
|
| 15 |
+
load_dotenv() # Add this line at the top, before other imports
|
| 16 |
+
|
| 17 |
+
# from fastapi.routing import ASGIRoute
|
| 18 |
from starlette.responses import Response
|
| 19 |
|
| 20 |
+
from fastmcp.server.http import StreamableHTTPSessionManager
|
| 21 |
+
from fastapi.responses import FileResponse
|
| 22 |
|
| 23 |
from .routes import auth, index, notes, search
|
| 24 |
from ..mcp.server import mcp
|
|
|
|
| 35 |
# CORS middleware
|
| 36 |
app.add_middleware(
|
| 37 |
CORSMiddleware,
|
| 38 |
+
allow_origins=[
|
| 39 |
+
"http://localhost:5173",
|
| 40 |
+
"http://localhost:3000",
|
| 41 |
+
"https://huggingface.co",
|
| 42 |
+
],
|
| 43 |
allow_credentials=True,
|
| 44 |
allow_methods=["*"],
|
| 45 |
allow_headers=["*"],
|
|
|
|
| 146 |
return {"status": "healthy"}
|
| 147 |
|
| 148 |
|
|
|
|
|
|
|
|
|
|
| 149 |
frontend_dist = Path(__file__).resolve().parents[3] / "frontend" / "dist"
|
| 150 |
if frontend_dist.exists():
|
| 151 |
# Mount static assets
|
| 152 |
+
app.mount(
|
| 153 |
+
"/assets", StaticFiles(directory=str(frontend_dist / "assets")), name="assets"
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
# Catch-all route for SPA - serve index.html for all non-API routes
|
| 157 |
@app.get("/{full_path:path}")
|
| 158 |
async def serve_spa(full_path: str):
|
| 159 |
"""Serve the SPA for all non-API routes."""
|
| 160 |
# Don't intercept API or auth routes
|
| 161 |
+
if (
|
| 162 |
+
full_path.startswith(("api/", "auth/"))
|
| 163 |
+
or full_path == "health"
|
| 164 |
+
or full_path.startswith("mcp/")
|
| 165 |
+
or full_path == "mcp"
|
| 166 |
+
):
|
| 167 |
# Let FastAPI's 404 handler take over
|
| 168 |
raise HTTPException(status_code=404, detail="Not found")
|
| 169 |
+
|
| 170 |
# If the path looks like a file (has extension), try to serve it
|
| 171 |
file_path = frontend_dist / full_path
|
| 172 |
if file_path.is_file():
|
| 173 |
return FileResponse(file_path)
|
| 174 |
# Otherwise serve index.html for SPA routing
|
| 175 |
return FileResponse(frontend_dist / "index.html")
|
| 176 |
+
|
| 177 |
logger.info(f"Serving frontend SPA from: {frontend_dist}")
|
| 178 |
else:
|
| 179 |
logger.warning(f"Frontend dist not found at: {frontend_dist}")
|
| 180 |
+
|
| 181 |
# Fallback health endpoint if no frontend
|
| 182 |
@app.get("/")
|
| 183 |
async def root():
|
|
|
|
| 186 |
|
| 187 |
|
| 188 |
__all__ = ["app"]
|
|
|
backend/src/api/routes/auth.py
CHANGED
|
@@ -15,7 +15,7 @@ from fastapi.responses import RedirectResponse
|
|
| 15 |
|
| 16 |
from ...models.auth import TokenResponse
|
| 17 |
from ...models.user import HFProfile, User
|
| 18 |
-
from ...services.auth import AuthService
|
| 19 |
from ...services.config import get_config
|
| 20 |
from ...services.seed import ensure_welcome_note
|
| 21 |
from ...services.vault import VaultService
|
|
@@ -35,7 +35,11 @@ def _create_oauth_state() -> str:
|
|
| 35 |
"""Generate a state token and store it with a timestamp."""
|
| 36 |
now = time.time()
|
| 37 |
# Garbage collect expired states
|
| 38 |
-
expired = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
for state in expired:
|
| 40 |
oauth_states.pop(state, None)
|
| 41 |
|
|
@@ -55,24 +59,24 @@ def _consume_oauth_state(state: str | None) -> None:
|
|
| 55 |
def get_base_url(request: Request) -> str:
|
| 56 |
"""
|
| 57 |
Get the base URL for OAuth redirects.
|
| 58 |
-
|
| 59 |
Uses the actual request URL scheme and hostname from FastAPI's request.url.
|
| 60 |
HF Spaces doesn't set X-Forwarded-Host, but the 'host' header is correct.
|
| 61 |
"""
|
| 62 |
# Get scheme from X-Forwarded-Proto or request
|
| 63 |
forwarded_proto = request.headers.get("x-forwarded-proto")
|
| 64 |
scheme = forwarded_proto if forwarded_proto else str(request.url.scheme)
|
| 65 |
-
|
| 66 |
# Get hostname from request URL (this comes from the 'host' header)
|
| 67 |
hostname = str(request.url.hostname)
|
| 68 |
-
|
| 69 |
# Check for port (but HF Spaces uses standard 443 for HTTPS)
|
| 70 |
port = request.url.port
|
| 71 |
if port and port not in (80, 443):
|
| 72 |
base_url = f"{scheme}://{hostname}:{port}"
|
| 73 |
else:
|
| 74 |
base_url = f"{scheme}://{hostname}"
|
| 75 |
-
|
| 76 |
logger.info(
|
| 77 |
f"OAuth base URL detected: {base_url}",
|
| 78 |
extra={
|
|
@@ -80,9 +84,9 @@ def get_base_url(request: Request) -> str:
|
|
| 80 |
"hostname": hostname,
|
| 81 |
"port": port,
|
| 82 |
"request_url": str(request.url),
|
| 83 |
-
}
|
| 84 |
)
|
| 85 |
-
|
| 86 |
return base_url
|
| 87 |
|
| 88 |
|
|
@@ -90,13 +94,13 @@ def get_base_url(request: Request) -> str:
|
|
| 90 |
async def login(request: Request):
|
| 91 |
"""Redirect to Hugging Face OAuth authorization page."""
|
| 92 |
config = get_config()
|
| 93 |
-
|
| 94 |
if not config.hf_oauth_client_id:
|
| 95 |
raise HTTPException(
|
| 96 |
status_code=501,
|
| 97 |
-
detail="OAuth not configured. Set HF_OAUTH_CLIENT_ID and HF_OAUTH_CLIENT_SECRET environment variables."
|
| 98 |
)
|
| 99 |
-
|
| 100 |
# Get base URL from request (handles HF Spaces proxy)
|
| 101 |
base_url = get_base_url(request)
|
| 102 |
redirect_uri = f"{base_url}/auth/callback"
|
|
@@ -112,7 +116,7 @@ async def login(request: Request):
|
|
| 112 |
"response_type": "code",
|
| 113 |
"state": state,
|
| 114 |
}
|
| 115 |
-
|
| 116 |
auth_url = f"{oauth_base}?{urlencode(params)}"
|
| 117 |
logger.info(
|
| 118 |
"Initiating OAuth flow",
|
|
@@ -121,9 +125,9 @@ async def login(request: Request):
|
|
| 121 |
"auth_url": auth_url,
|
| 122 |
"client_id": config.hf_oauth_client_id[:8] + "...",
|
| 123 |
"state": state,
|
| 124 |
-
}
|
| 125 |
)
|
| 126 |
-
|
| 127 |
return RedirectResponse(url=auth_url, status_code=302)
|
| 128 |
|
| 129 |
|
|
@@ -131,21 +135,20 @@ async def login(request: Request):
|
|
| 131 |
async def callback(
|
| 132 |
request: Request,
|
| 133 |
code: str = Query(..., description="OAuth authorization code"),
|
| 134 |
-
state: Optional[str] = Query(
|
|
|
|
|
|
|
| 135 |
):
|
| 136 |
"""Handle OAuth callback from Hugging Face."""
|
| 137 |
config = get_config()
|
| 138 |
-
|
| 139 |
if not config.hf_oauth_client_id or not config.hf_oauth_client_secret:
|
| 140 |
-
raise HTTPException(
|
| 141 |
-
|
| 142 |
-
detail="OAuth not configured"
|
| 143 |
-
)
|
| 144 |
-
|
| 145 |
# Get base URL from request (must match the one sent to HF)
|
| 146 |
base_url = get_base_url(request)
|
| 147 |
redirect_uri = f"{base_url}/auth/callback"
|
| 148 |
-
|
| 149 |
# Validate state token to prevent CSRF and replay attacks
|
| 150 |
_consume_oauth_state(state)
|
| 151 |
|
|
@@ -155,9 +158,9 @@ async def callback(
|
|
| 155 |
"redirect_uri": redirect_uri,
|
| 156 |
"state": state,
|
| 157 |
"code_length": len(code) if code else 0,
|
| 158 |
-
}
|
| 159 |
)
|
| 160 |
-
|
| 161 |
try:
|
| 162 |
# Exchange authorization code for access token
|
| 163 |
async with httpx.AsyncClient() as client:
|
|
@@ -171,50 +174,47 @@ async def callback(
|
|
| 171 |
"client_secret": config.hf_oauth_client_secret,
|
| 172 |
},
|
| 173 |
)
|
| 174 |
-
|
| 175 |
if token_response.status_code != 200:
|
| 176 |
logger.error(f"Token exchange failed: {token_response.text}")
|
| 177 |
raise HTTPException(
|
| 178 |
status_code=400,
|
| 179 |
-
detail="Failed to exchange authorization code for token"
|
| 180 |
)
|
| 181 |
-
|
| 182 |
token_data = token_response.json()
|
| 183 |
access_token = token_data.get("access_token")
|
| 184 |
-
|
| 185 |
if not access_token:
|
| 186 |
raise HTTPException(
|
| 187 |
-
status_code=400,
|
| 188 |
-
detail="No access token in response"
|
| 189 |
)
|
| 190 |
-
|
| 191 |
# Get user profile from HF
|
| 192 |
user_response = await client.get(
|
| 193 |
"https://huggingface.co/api/whoami-v2",
|
| 194 |
-
headers={"Authorization": f"Bearer {access_token}"}
|
| 195 |
)
|
| 196 |
-
|
| 197 |
if user_response.status_code != 200:
|
| 198 |
logger.error(f"User profile fetch failed: {user_response.text}")
|
| 199 |
raise HTTPException(
|
| 200 |
-
status_code=400,
|
| 201 |
-
detail="Failed to fetch user profile"
|
| 202 |
)
|
| 203 |
-
|
| 204 |
user_data = user_response.json()
|
| 205 |
username = user_data.get("name")
|
| 206 |
email = user_data.get("email")
|
| 207 |
-
|
| 208 |
if not username:
|
| 209 |
raise HTTPException(
|
| 210 |
-
status_code=400,
|
| 211 |
-
detail="No username in user profile"
|
| 212 |
)
|
| 213 |
-
|
| 214 |
# Create JWT for our application
|
| 215 |
import jwt
|
| 216 |
from datetime import datetime, timedelta, timezone
|
| 217 |
-
|
| 218 |
user_id = username # Use HF username as user_id
|
| 219 |
|
| 220 |
# Ensure the user has an initialized vault with a welcome note
|
|
@@ -237,36 +237,37 @@ async def callback(
|
|
| 237 |
"exp": datetime.now(timezone.utc) + timedelta(days=7),
|
| 238 |
"iat": datetime.now(timezone.utc),
|
| 239 |
}
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
logger.info(
|
| 244 |
"OAuth successful",
|
| 245 |
extra={
|
| 246 |
"username": username,
|
| 247 |
"user_id": user_id,
|
| 248 |
"email": email,
|
| 249 |
-
}
|
| 250 |
)
|
| 251 |
-
|
| 252 |
# Redirect to frontend with token in URL hash
|
| 253 |
frontend_url = base_url
|
| 254 |
redirect_url = f"{frontend_url}/#token={jwt_token}"
|
| 255 |
logger.info(f"Redirecting to frontend: {redirect_url}")
|
| 256 |
return RedirectResponse(url=redirect_url, status_code=302)
|
| 257 |
-
|
| 258 |
except httpx.HTTPError as e:
|
| 259 |
logger.exception(f"HTTP error during OAuth: {e}")
|
| 260 |
raise HTTPException(
|
| 261 |
-
status_code=500,
|
| 262 |
-
detail="OAuth flow failed due to network error"
|
| 263 |
)
|
| 264 |
except Exception as e:
|
| 265 |
logger.exception(f"Unexpected error during OAuth: {e}")
|
| 266 |
-
raise HTTPException(
|
| 267 |
-
status_code=500,
|
| 268 |
-
detail="OAuth flow failed"
|
| 269 |
-
)
|
| 270 |
|
| 271 |
|
| 272 |
@router.post("/api/tokens", response_model=TokenResponse)
|
|
@@ -310,4 +311,3 @@ async def get_current_user(auth: AuthContext = Depends(get_auth_context)):
|
|
| 310 |
|
| 311 |
|
| 312 |
__all__ = ["router"]
|
| 313 |
-
|
|
|
|
| 15 |
|
| 16 |
from ...models.auth import TokenResponse
|
| 17 |
from ...models.user import HFProfile, User
|
| 18 |
+
from ...services.auth import AuthError, AuthService
|
| 19 |
from ...services.config import get_config
|
| 20 |
from ...services.seed import ensure_welcome_note
|
| 21 |
from ...services.vault import VaultService
|
|
|
|
| 35 |
"""Generate a state token and store it with a timestamp."""
|
| 36 |
now = time.time()
|
| 37 |
# Garbage collect expired states
|
| 38 |
+
expired = [
|
| 39 |
+
state
|
| 40 |
+
for state, ts in oauth_states.items()
|
| 41 |
+
if now - ts > OAUTH_STATE_TTL_SECONDS
|
| 42 |
+
]
|
| 43 |
for state in expired:
|
| 44 |
oauth_states.pop(state, None)
|
| 45 |
|
|
|
|
| 59 |
def get_base_url(request: Request) -> str:
|
| 60 |
"""
|
| 61 |
Get the base URL for OAuth redirects.
|
| 62 |
+
|
| 63 |
Uses the actual request URL scheme and hostname from FastAPI's request.url.
|
| 64 |
HF Spaces doesn't set X-Forwarded-Host, but the 'host' header is correct.
|
| 65 |
"""
|
| 66 |
# Get scheme from X-Forwarded-Proto or request
|
| 67 |
forwarded_proto = request.headers.get("x-forwarded-proto")
|
| 68 |
scheme = forwarded_proto if forwarded_proto else str(request.url.scheme)
|
| 69 |
+
|
| 70 |
# Get hostname from request URL (this comes from the 'host' header)
|
| 71 |
hostname = str(request.url.hostname)
|
| 72 |
+
|
| 73 |
# Check for port (but HF Spaces uses standard 443 for HTTPS)
|
| 74 |
port = request.url.port
|
| 75 |
if port and port not in (80, 443):
|
| 76 |
base_url = f"{scheme}://{hostname}:{port}"
|
| 77 |
else:
|
| 78 |
base_url = f"{scheme}://{hostname}"
|
| 79 |
+
|
| 80 |
logger.info(
|
| 81 |
f"OAuth base URL detected: {base_url}",
|
| 82 |
extra={
|
|
|
|
| 84 |
"hostname": hostname,
|
| 85 |
"port": port,
|
| 86 |
"request_url": str(request.url),
|
| 87 |
+
},
|
| 88 |
)
|
| 89 |
+
|
| 90 |
return base_url
|
| 91 |
|
| 92 |
|
|
|
|
| 94 |
async def login(request: Request):
|
| 95 |
"""Redirect to Hugging Face OAuth authorization page."""
|
| 96 |
config = get_config()
|
| 97 |
+
|
| 98 |
if not config.hf_oauth_client_id:
|
| 99 |
raise HTTPException(
|
| 100 |
status_code=501,
|
| 101 |
+
detail="OAuth not configured. Set HF_OAUTH_CLIENT_ID and HF_OAUTH_CLIENT_SECRET environment variables.",
|
| 102 |
)
|
| 103 |
+
|
| 104 |
# Get base URL from request (handles HF Spaces proxy)
|
| 105 |
base_url = get_base_url(request)
|
| 106 |
redirect_uri = f"{base_url}/auth/callback"
|
|
|
|
| 116 |
"response_type": "code",
|
| 117 |
"state": state,
|
| 118 |
}
|
| 119 |
+
|
| 120 |
auth_url = f"{oauth_base}?{urlencode(params)}"
|
| 121 |
logger.info(
|
| 122 |
"Initiating OAuth flow",
|
|
|
|
| 125 |
"auth_url": auth_url,
|
| 126 |
"client_id": config.hf_oauth_client_id[:8] + "...",
|
| 127 |
"state": state,
|
| 128 |
+
},
|
| 129 |
)
|
| 130 |
+
|
| 131 |
return RedirectResponse(url=auth_url, status_code=302)
|
| 132 |
|
| 133 |
|
|
|
|
| 135 |
async def callback(
|
| 136 |
request: Request,
|
| 137 |
code: str = Query(..., description="OAuth authorization code"),
|
| 138 |
+
state: Optional[str] = Query(
|
| 139 |
+
None, description="State parameter for CSRF protection"
|
| 140 |
+
),
|
| 141 |
):
|
| 142 |
"""Handle OAuth callback from Hugging Face."""
|
| 143 |
config = get_config()
|
| 144 |
+
|
| 145 |
if not config.hf_oauth_client_id or not config.hf_oauth_client_secret:
|
| 146 |
+
raise HTTPException(status_code=501, detail="OAuth not configured")
|
| 147 |
+
|
|
|
|
|
|
|
|
|
|
| 148 |
# Get base URL from request (must match the one sent to HF)
|
| 149 |
base_url = get_base_url(request)
|
| 150 |
redirect_uri = f"{base_url}/auth/callback"
|
| 151 |
+
|
| 152 |
# Validate state token to prevent CSRF and replay attacks
|
| 153 |
_consume_oauth_state(state)
|
| 154 |
|
|
|
|
| 158 |
"redirect_uri": redirect_uri,
|
| 159 |
"state": state,
|
| 160 |
"code_length": len(code) if code else 0,
|
| 161 |
+
},
|
| 162 |
)
|
| 163 |
+
|
| 164 |
try:
|
| 165 |
# Exchange authorization code for access token
|
| 166 |
async with httpx.AsyncClient() as client:
|
|
|
|
| 174 |
"client_secret": config.hf_oauth_client_secret,
|
| 175 |
},
|
| 176 |
)
|
| 177 |
+
|
| 178 |
if token_response.status_code != 200:
|
| 179 |
logger.error(f"Token exchange failed: {token_response.text}")
|
| 180 |
raise HTTPException(
|
| 181 |
status_code=400,
|
| 182 |
+
detail="Failed to exchange authorization code for token",
|
| 183 |
)
|
| 184 |
+
|
| 185 |
token_data = token_response.json()
|
| 186 |
access_token = token_data.get("access_token")
|
| 187 |
+
|
| 188 |
if not access_token:
|
| 189 |
raise HTTPException(
|
| 190 |
+
status_code=400, detail="No access token in response"
|
|
|
|
| 191 |
)
|
| 192 |
+
|
| 193 |
# Get user profile from HF
|
| 194 |
user_response = await client.get(
|
| 195 |
"https://huggingface.co/api/whoami-v2",
|
| 196 |
+
headers={"Authorization": f"Bearer {access_token}"},
|
| 197 |
)
|
| 198 |
+
|
| 199 |
if user_response.status_code != 200:
|
| 200 |
logger.error(f"User profile fetch failed: {user_response.text}")
|
| 201 |
raise HTTPException(
|
| 202 |
+
status_code=400, detail="Failed to fetch user profile"
|
|
|
|
| 203 |
)
|
| 204 |
+
|
| 205 |
user_data = user_response.json()
|
| 206 |
username = user_data.get("name")
|
| 207 |
email = user_data.get("email")
|
| 208 |
+
|
| 209 |
if not username:
|
| 210 |
raise HTTPException(
|
| 211 |
+
status_code=400, detail="No username in user profile"
|
|
|
|
| 212 |
)
|
| 213 |
+
|
| 214 |
# Create JWT for our application
|
| 215 |
import jwt
|
| 216 |
from datetime import datetime, timedelta, timezone
|
| 217 |
+
|
| 218 |
user_id = username # Use HF username as user_id
|
| 219 |
|
| 220 |
# Ensure the user has an initialized vault with a welcome note
|
|
|
|
| 237 |
"exp": datetime.now(timezone.utc) + timedelta(days=7),
|
| 238 |
"iat": datetime.now(timezone.utc),
|
| 239 |
}
|
| 240 |
+
|
| 241 |
+
try:
|
| 242 |
+
jwt_secret = auth_service._require_secret()
|
| 243 |
+
except AuthError as exc:
|
| 244 |
+
raise HTTPException(status_code=exc.status_code, detail=exc.message)
|
| 245 |
+
|
| 246 |
+
jwt_token = jwt.encode(payload, jwt_secret, algorithm="HS256")
|
| 247 |
+
|
| 248 |
logger.info(
|
| 249 |
"OAuth successful",
|
| 250 |
extra={
|
| 251 |
"username": username,
|
| 252 |
"user_id": user_id,
|
| 253 |
"email": email,
|
| 254 |
+
},
|
| 255 |
)
|
| 256 |
+
|
| 257 |
# Redirect to frontend with token in URL hash
|
| 258 |
frontend_url = base_url
|
| 259 |
redirect_url = f"{frontend_url}/#token={jwt_token}"
|
| 260 |
logger.info(f"Redirecting to frontend: {redirect_url}")
|
| 261 |
return RedirectResponse(url=redirect_url, status_code=302)
|
| 262 |
+
|
| 263 |
except httpx.HTTPError as e:
|
| 264 |
logger.exception(f"HTTP error during OAuth: {e}")
|
| 265 |
raise HTTPException(
|
| 266 |
+
status_code=500, detail="OAuth flow failed due to network error"
|
|
|
|
| 267 |
)
|
| 268 |
except Exception as e:
|
| 269 |
logger.exception(f"Unexpected error during OAuth: {e}")
|
| 270 |
+
raise HTTPException(status_code=500, detail="OAuth flow failed")
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
|
| 273 |
@router.post("/api/tokens", response_model=TokenResponse)
|
|
|
|
| 311 |
|
| 312 |
|
| 313 |
__all__ = ["router"]
|
|
|
backend/src/mcp/server.py
CHANGED
|
@@ -7,9 +7,13 @@ import os
|
|
| 7 |
import time
|
| 8 |
from typing import Any, Dict, List, Optional
|
| 9 |
|
|
|
|
| 10 |
from fastmcp import FastMCP
|
| 11 |
from pydantic import Field
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
from ..services import IndexerService, VaultNote, VaultService
|
| 14 |
from ..services.auth import AuthError, AuthService
|
| 15 |
|
|
@@ -72,7 +76,10 @@ def _note_to_response(note: VaultNote) -> Dict[str, Any]:
|
|
| 72 |
}
|
| 73 |
|
| 74 |
|
| 75 |
-
@mcp.tool(
|
|
|
|
|
|
|
|
|
|
| 76 |
def list_notes(
|
| 77 |
folder: Optional[str] = Field(
|
| 78 |
default=None,
|
|
@@ -81,9 +88,9 @@ def list_notes(
|
|
| 81 |
) -> List[Dict[str, Any]]:
|
| 82 |
start_time = time.time()
|
| 83 |
user_id = _current_user_id()
|
| 84 |
-
|
| 85 |
notes = vault_service.list_notes(user_id, folder=folder)
|
| 86 |
-
|
| 87 |
duration_ms = (time.time() - start_time) * 1000
|
| 88 |
logger.info(
|
| 89 |
"MCP tool called",
|
|
@@ -92,10 +99,10 @@ def list_notes(
|
|
| 92 |
"user_id": user_id,
|
| 93 |
"folder": folder or "(root)",
|
| 94 |
"result_count": len(notes),
|
| 95 |
-
"duration_ms": f"{duration_ms:.2f}"
|
| 96 |
-
}
|
| 97 |
)
|
| 98 |
-
|
| 99 |
return [
|
| 100 |
{
|
| 101 |
"path": entry["path"],
|
|
@@ -108,13 +115,15 @@ def list_notes(
|
|
| 108 |
|
| 109 |
@mcp.tool(name="read_note", description="Read a Markdown note with metadata and body.")
|
| 110 |
def read_note(
|
| 111 |
-
path: str = Field(
|
|
|
|
|
|
|
| 112 |
) -> Dict[str, Any]:
|
| 113 |
start_time = time.time()
|
| 114 |
user_id = _current_user_id()
|
| 115 |
-
|
| 116 |
note = vault_service.read_note(user_id, path)
|
| 117 |
-
|
| 118 |
duration_ms = (time.time() - start_time) * 1000
|
| 119 |
logger.info(
|
| 120 |
"MCP tool called",
|
|
@@ -122,10 +131,10 @@ def read_note(
|
|
| 122 |
"tool_name": "read_note",
|
| 123 |
"user_id": user_id,
|
| 124 |
"note_path": path,
|
| 125 |
-
"duration_ms": f"{duration_ms:.2f}"
|
| 126 |
-
}
|
| 127 |
)
|
| 128 |
-
|
| 129 |
return _note_to_response(note)
|
| 130 |
|
| 131 |
|
|
@@ -134,7 +143,9 @@ def read_note(
|
|
| 134 |
description="Create or update a note. Automatically updates frontmatter timestamps and search index.",
|
| 135 |
)
|
| 136 |
def write_note(
|
| 137 |
-
path: str = Field(
|
|
|
|
|
|
|
| 138 |
body: str = Field(..., description="Markdown body β€1 MiB."),
|
| 139 |
title: Optional[str] = Field(
|
| 140 |
default=None,
|
|
@@ -147,7 +158,7 @@ def write_note(
|
|
| 147 |
) -> Dict[str, Any]:
|
| 148 |
start_time = time.time()
|
| 149 |
user_id = _current_user_id()
|
| 150 |
-
|
| 151 |
note = vault_service.write_note(
|
| 152 |
user_id,
|
| 153 |
path,
|
|
@@ -156,7 +167,7 @@ def write_note(
|
|
| 156 |
body=body,
|
| 157 |
)
|
| 158 |
indexer_service.index_note(user_id, note)
|
| 159 |
-
|
| 160 |
duration_ms = (time.time() - start_time) * 1000
|
| 161 |
logger.info(
|
| 162 |
"MCP tool called",
|
|
@@ -164,23 +175,25 @@ def write_note(
|
|
| 164 |
"tool_name": "write_note",
|
| 165 |
"user_id": user_id,
|
| 166 |
"note_path": path,
|
| 167 |
-
"duration_ms": f"{duration_ms:.2f}"
|
| 168 |
-
}
|
| 169 |
)
|
| 170 |
-
|
| 171 |
return {"status": "ok", "path": path}
|
| 172 |
|
| 173 |
|
| 174 |
@mcp.tool(name="delete_note", description="Delete a note and remove it from the index.")
|
| 175 |
def delete_note(
|
| 176 |
-
path: str = Field(
|
|
|
|
|
|
|
| 177 |
) -> Dict[str, str]:
|
| 178 |
start_time = time.time()
|
| 179 |
user_id = _current_user_id()
|
| 180 |
-
|
| 181 |
vault_service.delete_note(user_id, path)
|
| 182 |
indexer_service.delete_note_index(user_id, path)
|
| 183 |
-
|
| 184 |
duration_ms = (time.time() - start_time) * 1000
|
| 185 |
logger.info(
|
| 186 |
"MCP tool called",
|
|
@@ -188,10 +201,10 @@ def delete_note(
|
|
| 188 |
"tool_name": "delete_note",
|
| 189 |
"user_id": user_id,
|
| 190 |
"note_path": path,
|
| 191 |
-
"duration_ms": f"{duration_ms:.2f}"
|
| 192 |
-
}
|
| 193 |
)
|
| 194 |
-
|
| 195 |
return {"status": "ok"}
|
| 196 |
|
| 197 |
|
|
@@ -205,9 +218,9 @@ def search_notes(
|
|
| 205 |
) -> List[Dict[str, Any]]:
|
| 206 |
start_time = time.time()
|
| 207 |
user_id = _current_user_id()
|
| 208 |
-
|
| 209 |
results = indexer_service.search_notes(user_id, query, limit=limit)
|
| 210 |
-
|
| 211 |
duration_ms = (time.time() - start_time) * 1000
|
| 212 |
logger.info(
|
| 213 |
"MCP tool called",
|
|
@@ -217,10 +230,10 @@ def search_notes(
|
|
| 217 |
"query": query,
|
| 218 |
"limit": limit,
|
| 219 |
"result_count": len(results),
|
| 220 |
-
"duration_ms": f"{duration_ms:.2f}"
|
| 221 |
-
}
|
| 222 |
)
|
| 223 |
-
|
| 224 |
return [
|
| 225 |
{
|
| 226 |
"path": row["path"],
|
|
@@ -231,9 +244,13 @@ def search_notes(
|
|
| 231 |
]
|
| 232 |
|
| 233 |
|
| 234 |
-
@mcp.tool(
|
|
|
|
|
|
|
| 235 |
def get_backlinks(
|
| 236 |
-
path: str = Field(
|
|
|
|
|
|
|
| 237 |
) -> List[Dict[str, Any]]:
|
| 238 |
user_id = _current_user_id()
|
| 239 |
backlinks = indexer_service.get_backlinks(user_id, path)
|
|
@@ -247,4 +264,17 @@ def get_tags() -> List[Dict[str, Any]]:
|
|
| 247 |
|
| 248 |
|
| 249 |
if __name__ == "__main__":
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
import time
|
| 8 |
from typing import Any, Dict, List, Optional
|
| 9 |
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
from fastmcp import FastMCP
|
| 12 |
from pydantic import Field
|
| 13 |
|
| 14 |
+
# Load environment variables from .env file
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
from ..services import IndexerService, VaultNote, VaultService
|
| 18 |
from ..services.auth import AuthError, AuthService
|
| 19 |
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
|
| 79 |
+
@mcp.tool(
|
| 80 |
+
name="list_notes",
|
| 81 |
+
description="List notes in the vault (optionally scoped to a folder).",
|
| 82 |
+
)
|
| 83 |
def list_notes(
|
| 84 |
folder: Optional[str] = Field(
|
| 85 |
default=None,
|
|
|
|
| 88 |
) -> List[Dict[str, Any]]:
|
| 89 |
start_time = time.time()
|
| 90 |
user_id = _current_user_id()
|
| 91 |
+
|
| 92 |
notes = vault_service.list_notes(user_id, folder=folder)
|
| 93 |
+
|
| 94 |
duration_ms = (time.time() - start_time) * 1000
|
| 95 |
logger.info(
|
| 96 |
"MCP tool called",
|
|
|
|
| 99 |
"user_id": user_id,
|
| 100 |
"folder": folder or "(root)",
|
| 101 |
"result_count": len(notes),
|
| 102 |
+
"duration_ms": f"{duration_ms:.2f}",
|
| 103 |
+
},
|
| 104 |
)
|
| 105 |
+
|
| 106 |
return [
|
| 107 |
{
|
| 108 |
"path": entry["path"],
|
|
|
|
| 115 |
|
| 116 |
@mcp.tool(name="read_note", description="Read a Markdown note with metadata and body.")
|
| 117 |
def read_note(
|
| 118 |
+
path: str = Field(
|
| 119 |
+
..., description="Relative '.md' path β€256 chars (no '..' or '\\')."
|
| 120 |
+
),
|
| 121 |
) -> Dict[str, Any]:
|
| 122 |
start_time = time.time()
|
| 123 |
user_id = _current_user_id()
|
| 124 |
+
|
| 125 |
note = vault_service.read_note(user_id, path)
|
| 126 |
+
|
| 127 |
duration_ms = (time.time() - start_time) * 1000
|
| 128 |
logger.info(
|
| 129 |
"MCP tool called",
|
|
|
|
| 131 |
"tool_name": "read_note",
|
| 132 |
"user_id": user_id,
|
| 133 |
"note_path": path,
|
| 134 |
+
"duration_ms": f"{duration_ms:.2f}",
|
| 135 |
+
},
|
| 136 |
)
|
| 137 |
+
|
| 138 |
return _note_to_response(note)
|
| 139 |
|
| 140 |
|
|
|
|
| 143 |
description="Create or update a note. Automatically updates frontmatter timestamps and search index.",
|
| 144 |
)
|
| 145 |
def write_note(
|
| 146 |
+
path: str = Field(
|
| 147 |
+
..., description="Relative '.md' path β€256 chars (no '..' or '\\')."
|
| 148 |
+
),
|
| 149 |
body: str = Field(..., description="Markdown body β€1 MiB."),
|
| 150 |
title: Optional[str] = Field(
|
| 151 |
default=None,
|
|
|
|
| 158 |
) -> Dict[str, Any]:
|
| 159 |
start_time = time.time()
|
| 160 |
user_id = _current_user_id()
|
| 161 |
+
|
| 162 |
note = vault_service.write_note(
|
| 163 |
user_id,
|
| 164 |
path,
|
|
|
|
| 167 |
body=body,
|
| 168 |
)
|
| 169 |
indexer_service.index_note(user_id, note)
|
| 170 |
+
|
| 171 |
duration_ms = (time.time() - start_time) * 1000
|
| 172 |
logger.info(
|
| 173 |
"MCP tool called",
|
|
|
|
| 175 |
"tool_name": "write_note",
|
| 176 |
"user_id": user_id,
|
| 177 |
"note_path": path,
|
| 178 |
+
"duration_ms": f"{duration_ms:.2f}",
|
| 179 |
+
},
|
| 180 |
)
|
| 181 |
+
|
| 182 |
return {"status": "ok", "path": path}
|
| 183 |
|
| 184 |
|
| 185 |
@mcp.tool(name="delete_note", description="Delete a note and remove it from the index.")
|
| 186 |
def delete_note(
|
| 187 |
+
path: str = Field(
|
| 188 |
+
..., description="Relative '.md' path β€256 chars (no '..' or '\\')."
|
| 189 |
+
),
|
| 190 |
) -> Dict[str, str]:
|
| 191 |
start_time = time.time()
|
| 192 |
user_id = _current_user_id()
|
| 193 |
+
|
| 194 |
vault_service.delete_note(user_id, path)
|
| 195 |
indexer_service.delete_note_index(user_id, path)
|
| 196 |
+
|
| 197 |
duration_ms = (time.time() - start_time) * 1000
|
| 198 |
logger.info(
|
| 199 |
"MCP tool called",
|
|
|
|
| 201 |
"tool_name": "delete_note",
|
| 202 |
"user_id": user_id,
|
| 203 |
"note_path": path,
|
| 204 |
+
"duration_ms": f"{duration_ms:.2f}",
|
| 205 |
+
},
|
| 206 |
)
|
| 207 |
+
|
| 208 |
return {"status": "ok"}
|
| 209 |
|
| 210 |
|
|
|
|
| 218 |
) -> List[Dict[str, Any]]:
|
| 219 |
start_time = time.time()
|
| 220 |
user_id = _current_user_id()
|
| 221 |
+
|
| 222 |
results = indexer_service.search_notes(user_id, query, limit=limit)
|
| 223 |
+
|
| 224 |
duration_ms = (time.time() - start_time) * 1000
|
| 225 |
logger.info(
|
| 226 |
"MCP tool called",
|
|
|
|
| 230 |
"query": query,
|
| 231 |
"limit": limit,
|
| 232 |
"result_count": len(results),
|
| 233 |
+
"duration_ms": f"{duration_ms:.2f}",
|
| 234 |
+
},
|
| 235 |
)
|
| 236 |
+
|
| 237 |
return [
|
| 238 |
{
|
| 239 |
"path": row["path"],
|
|
|
|
| 244 |
]
|
| 245 |
|
| 246 |
|
| 247 |
+
@mcp.tool(
|
| 248 |
+
name="get_backlinks", description="List notes that reference the target note."
|
| 249 |
+
)
|
| 250 |
def get_backlinks(
|
| 251 |
+
path: str = Field(
|
| 252 |
+
..., description="Relative '.md' path β€256 chars (no '..' or '\\')."
|
| 253 |
+
),
|
| 254 |
) -> List[Dict[str, Any]]:
|
| 255 |
user_id = _current_user_id()
|
| 256 |
backlinks = indexer_service.get_backlinks(user_id, path)
|
|
|
|
| 264 |
|
| 265 |
|
| 266 |
if __name__ == "__main__":
|
| 267 |
+
transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower() or "stdio"
|
| 268 |
+
|
| 269 |
+
# Configure HTTP transport with custom port if specified
|
| 270 |
+
if transport == "http":
|
| 271 |
+
port = int(os.getenv("MCP_PORT", "8001"))
|
| 272 |
+
host = os.getenv("MCP_HOST", "127.0.0.1")
|
| 273 |
+
logger.info(
|
| 274 |
+
"Starting MCP server",
|
| 275 |
+
extra={"transport": transport, "host": host, "port": port},
|
| 276 |
+
)
|
| 277 |
+
mcp.run(transport=transport, host=host, port=port)
|
| 278 |
+
else:
|
| 279 |
+
logger.info("Starting MCP server", extra={"transport": transport})
|
| 280 |
+
mcp.run(transport=transport)
|
backend/src/services/auth.py
CHANGED
|
@@ -44,9 +44,25 @@ class AuthService:
|
|
| 44 |
self.algorithm = algorithm
|
| 45 |
self.token_ttl_days = token_ttl_days
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
def _require_secret(self) -> str:
|
| 48 |
secret = self.config.jwt_secret_key
|
| 49 |
if not secret:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
raise AuthError(
|
| 51 |
"missing_jwt_secret",
|
| 52 |
"JWT secret is not configured; set JWT_SECRET_KEY to enable authentication features",
|
|
@@ -65,6 +81,14 @@ class AuthService:
|
|
| 65 |
exp=int((now + lifetime).timestamp()),
|
| 66 |
)
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
def create_jwt(self, user_id: str, *, expires_in: Optional[timedelta] = None) -> str:
|
| 69 |
"""Create a signed JWT for the given user."""
|
| 70 |
payload = self._build_payload(user_id, expires_in)
|
|
@@ -76,6 +100,9 @@ class AuthService:
|
|
| 76 |
|
| 77 |
def validate_jwt(self, token: str) -> JWTPayload:
|
| 78 |
"""Validate a JWT and return the decoded payload."""
|
|
|
|
|
|
|
|
|
|
| 79 |
try:
|
| 80 |
decoded = jwt.decode(
|
| 81 |
token,
|
|
|
|
| 44 |
self.algorithm = algorithm
|
| 45 |
self.token_ttl_days = token_ttl_days
|
| 46 |
|
| 47 |
+
@property
|
| 48 |
+
def _local_mode_enabled(self) -> bool:
|
| 49 |
+
return bool(self.config.enable_local_mode)
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def _local_dev_token(self) -> Optional[str]:
|
| 53 |
+
token = self.config.local_dev_token
|
| 54 |
+
return token.strip() if token else None
|
| 55 |
+
|
| 56 |
def _require_secret(self) -> str:
|
| 57 |
secret = self.config.jwt_secret_key
|
| 58 |
if not secret:
|
| 59 |
+
if self._local_mode_enabled and self._local_dev_token:
|
| 60 |
+
# Local mode can operate without JWT secret for read-only flows.
|
| 61 |
+
raise AuthError(
|
| 62 |
+
"missing_jwt_secret",
|
| 63 |
+
"JWT secret is not configured; set JWT_SECRET_KEY to enable JWT issuance",
|
| 64 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 65 |
+
)
|
| 66 |
raise AuthError(
|
| 67 |
"missing_jwt_secret",
|
| 68 |
"JWT secret is not configured; set JWT_SECRET_KEY to enable authentication features",
|
|
|
|
| 81 |
exp=int((now + lifetime).timestamp()),
|
| 82 |
)
|
| 83 |
|
| 84 |
+
def _local_dev_payload(self) -> JWTPayload:
|
| 85 |
+
now = datetime.now(timezone.utc)
|
| 86 |
+
return JWTPayload(
|
| 87 |
+
sub="local-dev",
|
| 88 |
+
iat=int(now.timestamp()),
|
| 89 |
+
exp=int((now + timedelta(days=365)).timestamp()),
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
def create_jwt(self, user_id: str, *, expires_in: Optional[timedelta] = None) -> str:
|
| 93 |
"""Create a signed JWT for the given user."""
|
| 94 |
payload = self._build_payload(user_id, expires_in)
|
|
|
|
| 100 |
|
| 101 |
def validate_jwt(self, token: str) -> JWTPayload:
|
| 102 |
"""Validate a JWT and return the decoded payload."""
|
| 103 |
+
if self._local_mode_enabled and self._local_dev_token:
|
| 104 |
+
if token == self._local_dev_token:
|
| 105 |
+
return self._local_dev_payload()
|
| 106 |
try:
|
| 107 |
decoded = jwt.decode(
|
| 108 |
token,
|
backend/src/services/config.py
CHANGED
|
@@ -22,6 +22,14 @@ class AppConfig(BaseModel):
|
|
| 22 |
default=None,
|
| 23 |
description="HMAC secret for JWT signing (required for JWT/HTTP auth)",
|
| 24 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
vault_base_path: Path = Field(..., description="Base directory for per-user vaults")
|
| 26 |
hf_oauth_client_id: Optional[str] = Field(
|
| 27 |
None, description="Hugging Face OAuth client ID (optional)"
|
|
@@ -72,9 +80,17 @@ def get_config() -> AppConfig:
|
|
| 72 |
hf_client_id = _read_env("HF_OAUTH_CLIENT_ID")
|
| 73 |
hf_client_secret = _read_env("HF_OAUTH_CLIENT_SECRET")
|
| 74 |
hf_space_url = _read_env("HF_SPACE_URL", "http://localhost:5173")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
config = AppConfig(
|
| 77 |
jwt_secret_key=jwt_secret,
|
|
|
|
|
|
|
| 78 |
vault_base_path=vault_base,
|
| 79 |
hf_oauth_client_id=hf_client_id,
|
| 80 |
hf_oauth_client_secret=hf_client_secret,
|
|
|
|
| 22 |
default=None,
|
| 23 |
description="HMAC secret for JWT signing (required for JWT/HTTP auth)",
|
| 24 |
)
|
| 25 |
+
enable_local_mode: bool = Field(
|
| 26 |
+
default=True,
|
| 27 |
+
description="Allow local-dev token bypass when running locally",
|
| 28 |
+
)
|
| 29 |
+
local_dev_token: Optional[str] = Field(
|
| 30 |
+
default="local-dev-token",
|
| 31 |
+
description="Static token accepted in local mode for development",
|
| 32 |
+
)
|
| 33 |
vault_base_path: Path = Field(..., description="Base directory for per-user vaults")
|
| 34 |
hf_oauth_client_id: Optional[str] = Field(
|
| 35 |
None, description="Hugging Face OAuth client ID (optional)"
|
|
|
|
| 80 |
hf_client_id = _read_env("HF_OAUTH_CLIENT_ID")
|
| 81 |
hf_client_secret = _read_env("HF_OAUTH_CLIENT_SECRET")
|
| 82 |
hf_space_url = _read_env("HF_SPACE_URL", "http://localhost:5173")
|
| 83 |
+
enable_local_mode = _read_env("ENABLE_LOCAL_MODE", "true").lower() not in {
|
| 84 |
+
"0",
|
| 85 |
+
"false",
|
| 86 |
+
"no",
|
| 87 |
+
}
|
| 88 |
+
local_dev_token = _read_env("LOCAL_DEV_TOKEN", "local-dev-token")
|
| 89 |
|
| 90 |
config = AppConfig(
|
| 91 |
jwt_secret_key=jwt_secret,
|
| 92 |
+
enable_local_mode=enable_local_mode,
|
| 93 |
+
local_dev_token=local_dev_token,
|
| 94 |
vault_base_path=vault_base,
|
| 95 |
hf_oauth_client_id=hf_client_id,
|
| 96 |
hf_oauth_client_secret=hf_client_secret,
|
backend/uv.lock
CHANGED
|
@@ -55,43 +55,6 @@ wheels = [
|
|
| 55 |
{ url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
|
| 56 |
]
|
| 57 |
|
| 58 |
-
[[package]]
|
| 59 |
-
name = "backend"
|
| 60 |
-
version = "0.1.0"
|
| 61 |
-
source = { virtual = "." }
|
| 62 |
-
dependencies = [
|
| 63 |
-
{ name = "fastapi" },
|
| 64 |
-
{ name = "fastmcp" },
|
| 65 |
-
{ name = "huggingface-hub" },
|
| 66 |
-
{ name = "pyjwt" },
|
| 67 |
-
{ name = "python-frontmatter" },
|
| 68 |
-
{ name = "uvicorn", extra = ["standard"] },
|
| 69 |
-
]
|
| 70 |
-
|
| 71 |
-
[package.dev-dependencies]
|
| 72 |
-
dev = [
|
| 73 |
-
{ name = "httpx" },
|
| 74 |
-
{ name = "pytest" },
|
| 75 |
-
{ name = "pytest-asyncio" },
|
| 76 |
-
]
|
| 77 |
-
|
| 78 |
-
[package.metadata]
|
| 79 |
-
requires-dist = [
|
| 80 |
-
{ name = "fastapi", specifier = ">=0.121.2" },
|
| 81 |
-
{ name = "fastmcp", specifier = ">=2.13.1" },
|
| 82 |
-
{ name = "huggingface-hub", specifier = ">=1.1.4" },
|
| 83 |
-
{ name = "pyjwt", specifier = ">=2.10.1" },
|
| 84 |
-
{ name = "python-frontmatter", specifier = ">=1.1.0" },
|
| 85 |
-
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.38.0" },
|
| 86 |
-
]
|
| 87 |
-
|
| 88 |
-
[package.metadata.requires-dev]
|
| 89 |
-
dev = [
|
| 90 |
-
{ name = "httpx", specifier = ">=0.28.1" },
|
| 91 |
-
{ name = "pytest", specifier = ">=9.0.1" },
|
| 92 |
-
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
|
| 93 |
-
]
|
| 94 |
-
|
| 95 |
[[package]]
|
| 96 |
name = "backports-tarfile"
|
| 97 |
version = "1.2.0"
|
|
@@ -396,6 +359,47 @@ wheels = [
|
|
| 396 |
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
|
| 397 |
]
|
| 398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
[[package]]
|
| 400 |
name = "docutils"
|
| 401 |
version = "0.22.3"
|
|
@@ -1298,7 +1302,7 @@ wheels = [
|
|
| 1298 |
{ url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" },
|
| 1299 |
{ url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" },
|
| 1300 |
{ url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" },
|
| 1301 |
-
{ url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size =
|
| 1302 |
{ url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" },
|
| 1303 |
{ url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" },
|
| 1304 |
{ url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" },
|
|
@@ -1537,7 +1541,7 @@ wheels = [
|
|
| 1537 |
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
|
| 1538 |
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
|
| 1539 |
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
|
| 1540 |
-
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:
|
| 1541 |
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
|
| 1542 |
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
|
| 1543 |
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
|
|
|
|
| 55 |
{ url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
|
| 56 |
]
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
[[package]]
|
| 59 |
name = "backports-tarfile"
|
| 60 |
version = "1.2.0"
|
|
|
|
| 359 |
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
|
| 360 |
]
|
| 361 |
|
| 362 |
+
[[package]]
|
| 363 |
+
name = "documentation-mcp"
|
| 364 |
+
version = "0.1.0"
|
| 365 |
+
source = { virtual = "." }
|
| 366 |
+
dependencies = [
|
| 367 |
+
{ name = "fastapi" },
|
| 368 |
+
{ name = "fastmcp" },
|
| 369 |
+
{ name = "huggingface-hub" },
|
| 370 |
+
{ name = "mcp" },
|
| 371 |
+
{ name = "pyjwt" },
|
| 372 |
+
{ name = "python-dotenv" },
|
| 373 |
+
{ name = "python-frontmatter" },
|
| 374 |
+
{ name = "uvicorn", extra = ["standard"] },
|
| 375 |
+
]
|
| 376 |
+
|
| 377 |
+
[package.dev-dependencies]
|
| 378 |
+
dev = [
|
| 379 |
+
{ name = "httpx" },
|
| 380 |
+
{ name = "pytest" },
|
| 381 |
+
{ name = "pytest-asyncio" },
|
| 382 |
+
]
|
| 383 |
+
|
| 384 |
+
[package.metadata]
|
| 385 |
+
requires-dist = [
|
| 386 |
+
{ name = "fastapi", specifier = ">=0.121.2" },
|
| 387 |
+
{ name = "fastmcp", specifier = ">=2.13.1" },
|
| 388 |
+
{ name = "huggingface-hub", specifier = ">=1.1.4" },
|
| 389 |
+
{ name = "mcp", specifier = ">=1.21.0" },
|
| 390 |
+
{ name = "pyjwt", specifier = ">=2.10.1" },
|
| 391 |
+
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
| 392 |
+
{ name = "python-frontmatter", specifier = ">=1.1.0" },
|
| 393 |
+
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.38.0" },
|
| 394 |
+
]
|
| 395 |
+
|
| 396 |
+
[package.metadata.requires-dev]
|
| 397 |
+
dev = [
|
| 398 |
+
{ name = "httpx", specifier = ">=0.28.1" },
|
| 399 |
+
{ name = "pytest", specifier = ">=9.0.1" },
|
| 400 |
+
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
|
| 401 |
+
]
|
| 402 |
+
|
| 403 |
[[package]]
|
| 404 |
name = "docutils"
|
| 405 |
version = "0.22.3"
|
|
|
|
| 1302 |
{ url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" },
|
| 1303 |
{ url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" },
|
| 1304 |
{ url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" },
|
| 1305 |
+
{ url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518001, upload-time = "2025-10-22T22:22:11.326Z" },
|
| 1306 |
{ url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" },
|
| 1307 |
{ url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" },
|
| 1308 |
{ url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" },
|
|
|
|
| 1541 |
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
|
| 1542 |
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
|
| 1543 |
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
|
| 1544 |
+
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8001ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
|
| 1545 |
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
|
| 1546 |
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
|
| 1547 |
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
|
debug_endpoints.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Debug script to check which endpoints are working."""
|
| 3 |
+
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
from urllib.parse import urljoin
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_endpoint(url, method="GET", headers=None, data=None, timeout=5):
|
| 10 |
+
"""Test a single endpoint and return detailed results."""
|
| 11 |
+
try:
|
| 12 |
+
if method.upper() == "GET":
|
| 13 |
+
response = requests.get(url, headers=headers, timeout=timeout)
|
| 14 |
+
elif method.upper() == "POST":
|
| 15 |
+
response = requests.post(url, headers=headers, json=data, timeout=timeout)
|
| 16 |
+
|
| 17 |
+
return {
|
| 18 |
+
"status": "SUCCESS",
|
| 19 |
+
"status_code": response.status_code,
|
| 20 |
+
"response": response.text[:500], # Limit response length
|
| 21 |
+
"headers": dict(response.headers),
|
| 22 |
+
}
|
| 23 |
+
except requests.exceptions.ConnectionError:
|
| 24 |
+
return {"status": "CONNECTION_ERROR", "error": "Cannot connect to server"}
|
| 25 |
+
except requests.exceptions.Timeout:
|
| 26 |
+
return {"status": "TIMEOUT", "error": "Request timed out"}
|
| 27 |
+
except Exception as e:
|
| 28 |
+
return {"status": "ERROR", "error": str(e)}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def check_all_endpoints():
|
| 32 |
+
"""Check all possible endpoints systematically."""
|
| 33 |
+
|
| 34 |
+
print("π Debugging Endpoint Availability")
|
| 35 |
+
print("=" * 60)
|
| 36 |
+
|
| 37 |
+
# Define test cases
|
| 38 |
+
test_cases = [
|
| 39 |
+
# FastAPI Server (Port 8000)
|
| 40 |
+
{
|
| 41 |
+
"name": "FastAPI Health",
|
| 42 |
+
"url": "http://localhost:8000/health",
|
| 43 |
+
"method": "GET",
|
| 44 |
+
},
|
| 45 |
+
{"name": "FastAPI Root", "url": "http://localhost:8000/", "method": "GET"},
|
| 46 |
+
{
|
| 47 |
+
"name": "FastAPI API Root",
|
| 48 |
+
"url": "http://localhost:8000/api/",
|
| 49 |
+
"method": "GET",
|
| 50 |
+
},
|
| 51 |
+
# MCP Server (Port 8001)
|
| 52 |
+
{"name": "MCP Root", "url": "http://localhost:8001/", "method": "GET"},
|
| 53 |
+
{"name": "MCP Endpoint", "url": "http://localhost:8001/mcp", "method": "GET"},
|
| 54 |
+
{
|
| 55 |
+
"name": "MCP Health (if exists)",
|
| 56 |
+
"url": "http://localhost:8001/health",
|
| 57 |
+
"method": "GET",
|
| 58 |
+
},
|
| 59 |
+
# MCP Protocol Tests
|
| 60 |
+
{
|
| 61 |
+
"name": "MCP Initialize",
|
| 62 |
+
"url": "http://localhost:8001/mcp",
|
| 63 |
+
"method": "POST",
|
| 64 |
+
"headers": {
|
| 65 |
+
"Authorization": "Bearer local-dev-token",
|
| 66 |
+
"Content-Type": "application/json",
|
| 67 |
+
"Accept": "application/json, text/event-stream",
|
| 68 |
+
},
|
| 69 |
+
"data": {
|
| 70 |
+
"jsonrpc": "2.0",
|
| 71 |
+
"id": 1,
|
| 72 |
+
"method": "initialize",
|
| 73 |
+
"params": {
|
| 74 |
+
"protocolVersion": "2024-11-05",
|
| 75 |
+
"capabilities": {},
|
| 76 |
+
"clientInfo": {"name": "debug-client", "version": "1.0.0"},
|
| 77 |
+
},
|
| 78 |
+
},
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"name": "MCP Tools List",
|
| 82 |
+
"url": "http://localhost:8001/mcp",
|
| 83 |
+
"method": "POST",
|
| 84 |
+
"headers": {
|
| 85 |
+
"Authorization": "Bearer local-dev-token",
|
| 86 |
+
"Content-Type": "application/json",
|
| 87 |
+
"Accept": "application/json, text/event-stream",
|
| 88 |
+
},
|
| 89 |
+
"data": {"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
|
| 90 |
+
},
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
results = {}
|
| 94 |
+
|
| 95 |
+
for test in test_cases:
|
| 96 |
+
print(f"\nπ§ͺ Testing: {test['name']}")
|
| 97 |
+
print(f" URL: {test['url']}")
|
| 98 |
+
print(f" Method: {test['method']}")
|
| 99 |
+
|
| 100 |
+
result = test_endpoint(
|
| 101 |
+
url=test["url"],
|
| 102 |
+
method=test["method"],
|
| 103 |
+
headers=test.get("headers"),
|
| 104 |
+
data=test.get("data"),
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
results[test["name"]] = result
|
| 108 |
+
|
| 109 |
+
if result["status"] == "SUCCESS":
|
| 110 |
+
print(f" β
Status: {result['status_code']}")
|
| 111 |
+
if result["response"]:
|
| 112 |
+
# Try to pretty print JSON
|
| 113 |
+
try:
|
| 114 |
+
json_resp = json.loads(result["response"])
|
| 115 |
+
print(f" π Response: {json.dumps(json_resp, indent=2)[:200]}...")
|
| 116 |
+
except:
|
| 117 |
+
print(f" π Response: {result['response'][:100]}...")
|
| 118 |
+
else:
|
| 119 |
+
print(f" β {result['status']}: {result.get('error', 'Unknown error')}")
|
| 120 |
+
|
| 121 |
+
print("\n" + "=" * 60)
|
| 122 |
+
print("π SUMMARY")
|
| 123 |
+
print("=" * 60)
|
| 124 |
+
|
| 125 |
+
working = []
|
| 126 |
+
not_working = []
|
| 127 |
+
|
| 128 |
+
for name, result in results.items():
|
| 129 |
+
if result["status"] == "SUCCESS" and result.get("status_code", 0) < 400:
|
| 130 |
+
working.append(name)
|
| 131 |
+
else:
|
| 132 |
+
not_working.append(name)
|
| 133 |
+
|
| 134 |
+
print(f"\nβ
WORKING ({len(working)}):")
|
| 135 |
+
for name in working:
|
| 136 |
+
status_code = results[name].get("status_code", "N/A")
|
| 137 |
+
print(f" - {name} (HTTP {status_code})")
|
| 138 |
+
|
| 139 |
+
print(f"\nβ NOT WORKING ({len(not_working)}):")
|
| 140 |
+
for name in not_working:
|
| 141 |
+
result = results[name]
|
| 142 |
+
if result["status"] == "SUCCESS":
|
| 143 |
+
print(f" - {name} (HTTP {result.get('status_code', 'N/A')})")
|
| 144 |
+
else:
|
| 145 |
+
print(f" - {name} ({result['status']})")
|
| 146 |
+
|
| 147 |
+
print(f"\nπ§ RECOMMENDATIONS:")
|
| 148 |
+
|
| 149 |
+
# Check if any server is running
|
| 150 |
+
fastapi_working = any("FastAPI" in name for name in working)
|
| 151 |
+
mcp_working = any("MCP" in name for name in working)
|
| 152 |
+
|
| 153 |
+
if not fastapi_working:
|
| 154 |
+
print(" - Start FastAPI server: cd backend && python main.py")
|
| 155 |
+
|
| 156 |
+
if not mcp_working:
|
| 157 |
+
print(
|
| 158 |
+
" - Start MCP server: cd backend && MCP_TRANSPORT=http MCP_PORT=8001 python -m src.mcp.server"
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
if not working:
|
| 162 |
+
print(" - Check if any servers are running: netstat -ano | findstr :8000")
|
| 163 |
+
print(" - Check if any servers are running: netstat -ano | findstr :8001")
|
| 164 |
+
|
| 165 |
+
return results
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
if __name__ == "__main__":
|
| 169 |
+
check_all_endpoints()
|
frontend/package-lock.json
CHANGED
|
@@ -118,7 +118,6 @@
|
|
| 118 |
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
| 119 |
"dev": true,
|
| 120 |
"license": "MIT",
|
| 121 |
-
"peer": true,
|
| 122 |
"dependencies": {
|
| 123 |
"@babel/code-frame": "^7.27.1",
|
| 124 |
"@babel/generator": "^7.28.5",
|
|
@@ -710,7 +709,6 @@
|
|
| 710 |
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 711 |
"dev": true,
|
| 712 |
"license": "MIT",
|
| 713 |
-
"peer": true,
|
| 714 |
"engines": {
|
| 715 |
"node": ">=12"
|
| 716 |
},
|
|
@@ -1736,7 +1734,6 @@
|
|
| 1736 |
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
| 1737 |
"dev": true,
|
| 1738 |
"license": "MIT",
|
| 1739 |
-
"peer": true,
|
| 1740 |
"engines": {
|
| 1741 |
"node": "^14.21.3 || >=16"
|
| 1742 |
},
|
|
@@ -3324,7 +3321,6 @@
|
|
| 3324 |
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
| 3325 |
"devOptional": true,
|
| 3326 |
"license": "MIT",
|
| 3327 |
-
"peer": true,
|
| 3328 |
"dependencies": {
|
| 3329 |
"undici-types": "~7.16.0"
|
| 3330 |
}
|
|
@@ -3334,7 +3330,6 @@
|
|
| 3334 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz",
|
| 3335 |
"integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==",
|
| 3336 |
"license": "MIT",
|
| 3337 |
-
"peer": true,
|
| 3338 |
"dependencies": {
|
| 3339 |
"csstype": "^3.0.2"
|
| 3340 |
}
|
|
@@ -3345,7 +3340,6 @@
|
|
| 3345 |
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
| 3346 |
"devOptional": true,
|
| 3347 |
"license": "MIT",
|
| 3348 |
-
"peer": true,
|
| 3349 |
"peerDependencies": {
|
| 3350 |
"@types/react": "^19.2.0"
|
| 3351 |
}
|
|
@@ -3409,7 +3403,6 @@
|
|
| 3409 |
"integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
|
| 3410 |
"dev": true,
|
| 3411 |
"license": "MIT",
|
| 3412 |
-
"peer": true,
|
| 3413 |
"dependencies": {
|
| 3414 |
"@typescript-eslint/scope-manager": "8.46.4",
|
| 3415 |
"@typescript-eslint/types": "8.46.4",
|
|
@@ -3682,7 +3675,6 @@
|
|
| 3682 |
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
| 3683 |
"dev": true,
|
| 3684 |
"license": "MIT",
|
| 3685 |
-
"peer": true,
|
| 3686 |
"bin": {
|
| 3687 |
"acorn": "bin/acorn"
|
| 3688 |
},
|
|
@@ -4003,7 +3995,6 @@
|
|
| 4003 |
}
|
| 4004 |
],
|
| 4005 |
"license": "MIT",
|
| 4006 |
-
"peer": true,
|
| 4007 |
"dependencies": {
|
| 4008 |
"baseline-browser-mapping": "^2.8.25",
|
| 4009 |
"caniuse-lite": "^1.0.30001754",
|
|
@@ -4878,7 +4869,6 @@
|
|
| 4878 |
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
| 4879 |
"dev": true,
|
| 4880 |
"license": "MIT",
|
| 4881 |
-
"peer": true,
|
| 4882 |
"dependencies": {
|
| 4883 |
"@eslint-community/eslint-utils": "^4.8.0",
|
| 4884 |
"@eslint-community/regexpp": "^4.12.1",
|
|
@@ -5147,7 +5137,6 @@
|
|
| 5147 |
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
| 5148 |
"dev": true,
|
| 5149 |
"license": "MIT",
|
| 5150 |
-
"peer": true,
|
| 5151 |
"dependencies": {
|
| 5152 |
"accepts": "^2.0.0",
|
| 5153 |
"body-parser": "^2.2.0",
|
|
@@ -5630,9 +5619,9 @@
|
|
| 5630 |
}
|
| 5631 |
},
|
| 5632 |
"node_modules/glob": {
|
| 5633 |
-
"version": "10.
|
| 5634 |
-
"resolved": "https://registry.npmjs.org/glob/-/glob-10.
|
| 5635 |
-
"integrity": "sha512-
|
| 5636 |
"license": "ISC",
|
| 5637 |
"dependencies": {
|
| 5638 |
"foreground-child": "^3.1.0",
|
|
@@ -6202,7 +6191,6 @@
|
|
| 6202 |
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
| 6203 |
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
| 6204 |
"license": "MIT",
|
| 6205 |
-
"peer": true,
|
| 6206 |
"bin": {
|
| 6207 |
"jiti": "bin/jiti.js"
|
| 6208 |
}
|
|
@@ -8043,7 +8031,6 @@
|
|
| 8043 |
}
|
| 8044 |
],
|
| 8045 |
"license": "MIT",
|
| 8046 |
-
"peer": true,
|
| 8047 |
"dependencies": {
|
| 8048 |
"nanoid": "^3.3.11",
|
| 8049 |
"picocolors": "^1.1.1",
|
|
@@ -8355,7 +8342,6 @@
|
|
| 8355 |
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
| 8356 |
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
| 8357 |
"license": "MIT",
|
| 8358 |
-
"peer": true,
|
| 8359 |
"engines": {
|
| 8360 |
"node": ">=0.10.0"
|
| 8361 |
}
|
|
@@ -8365,7 +8351,6 @@
|
|
| 8365 |
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
| 8366 |
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
| 8367 |
"license": "MIT",
|
| 8368 |
-
"peer": true,
|
| 8369 |
"dependencies": {
|
| 8370 |
"scheduler": "^0.27.0"
|
| 8371 |
},
|
|
@@ -9408,7 +9393,6 @@
|
|
| 9408 |
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
| 9409 |
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
| 9410 |
"license": "MIT",
|
| 9411 |
-
"peer": true,
|
| 9412 |
"dependencies": {
|
| 9413 |
"@alloc/quick-lru": "^5.2.0",
|
| 9414 |
"arg": "^5.0.2",
|
|
@@ -9540,7 +9524,6 @@
|
|
| 9540 |
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 9541 |
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 9542 |
"license": "MIT",
|
| 9543 |
-
"peer": true,
|
| 9544 |
"engines": {
|
| 9545 |
"node": ">=12"
|
| 9546 |
},
|
|
@@ -9720,7 +9703,6 @@
|
|
| 9720 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 9721 |
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 9722 |
"license": "Apache-2.0",
|
| 9723 |
-
"peer": true,
|
| 9724 |
"bin": {
|
| 9725 |
"tsc": "bin/tsc",
|
| 9726 |
"tsserver": "bin/tsserver"
|
|
@@ -10123,7 +10105,6 @@
|
|
| 10123 |
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 10124 |
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 10125 |
"license": "MIT",
|
| 10126 |
-
"peer": true,
|
| 10127 |
"engines": {
|
| 10128 |
"node": ">=12"
|
| 10129 |
},
|
|
@@ -10314,7 +10295,6 @@
|
|
| 10314 |
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
| 10315 |
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
| 10316 |
"license": "ISC",
|
| 10317 |
-
"peer": true,
|
| 10318 |
"bin": {
|
| 10319 |
"yaml": "bin.mjs"
|
| 10320 |
},
|
|
@@ -10441,7 +10421,6 @@
|
|
| 10441 |
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
| 10442 |
"dev": true,
|
| 10443 |
"license": "MIT",
|
| 10444 |
-
"peer": true,
|
| 10445 |
"funding": {
|
| 10446 |
"url": "https://github.com/sponsors/colinhacks"
|
| 10447 |
}
|
|
|
|
| 118 |
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
| 119 |
"dev": true,
|
| 120 |
"license": "MIT",
|
|
|
|
| 121 |
"dependencies": {
|
| 122 |
"@babel/code-frame": "^7.27.1",
|
| 123 |
"@babel/generator": "^7.28.5",
|
|
|
|
| 709 |
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 710 |
"dev": true,
|
| 711 |
"license": "MIT",
|
|
|
|
| 712 |
"engines": {
|
| 713 |
"node": ">=12"
|
| 714 |
},
|
|
|
|
| 1734 |
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
| 1735 |
"dev": true,
|
| 1736 |
"license": "MIT",
|
|
|
|
| 1737 |
"engines": {
|
| 1738 |
"node": "^14.21.3 || >=16"
|
| 1739 |
},
|
|
|
|
| 3321 |
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
| 3322 |
"devOptional": true,
|
| 3323 |
"license": "MIT",
|
|
|
|
| 3324 |
"dependencies": {
|
| 3325 |
"undici-types": "~7.16.0"
|
| 3326 |
}
|
|
|
|
| 3330 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz",
|
| 3331 |
"integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==",
|
| 3332 |
"license": "MIT",
|
|
|
|
| 3333 |
"dependencies": {
|
| 3334 |
"csstype": "^3.0.2"
|
| 3335 |
}
|
|
|
|
| 3340 |
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
| 3341 |
"devOptional": true,
|
| 3342 |
"license": "MIT",
|
|
|
|
| 3343 |
"peerDependencies": {
|
| 3344 |
"@types/react": "^19.2.0"
|
| 3345 |
}
|
|
|
|
| 3403 |
"integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
|
| 3404 |
"dev": true,
|
| 3405 |
"license": "MIT",
|
|
|
|
| 3406 |
"dependencies": {
|
| 3407 |
"@typescript-eslint/scope-manager": "8.46.4",
|
| 3408 |
"@typescript-eslint/types": "8.46.4",
|
|
|
|
| 3675 |
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
| 3676 |
"dev": true,
|
| 3677 |
"license": "MIT",
|
|
|
|
| 3678 |
"bin": {
|
| 3679 |
"acorn": "bin/acorn"
|
| 3680 |
},
|
|
|
|
| 3995 |
}
|
| 3996 |
],
|
| 3997 |
"license": "MIT",
|
|
|
|
| 3998 |
"dependencies": {
|
| 3999 |
"baseline-browser-mapping": "^2.8.25",
|
| 4000 |
"caniuse-lite": "^1.0.30001754",
|
|
|
|
| 4869 |
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
| 4870 |
"dev": true,
|
| 4871 |
"license": "MIT",
|
|
|
|
| 4872 |
"dependencies": {
|
| 4873 |
"@eslint-community/eslint-utils": "^4.8.0",
|
| 4874 |
"@eslint-community/regexpp": "^4.12.1",
|
|
|
|
| 5137 |
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
| 5138 |
"dev": true,
|
| 5139 |
"license": "MIT",
|
|
|
|
| 5140 |
"dependencies": {
|
| 5141 |
"accepts": "^2.0.0",
|
| 5142 |
"body-parser": "^2.2.0",
|
|
|
|
| 5619 |
}
|
| 5620 |
},
|
| 5621 |
"node_modules/glob": {
|
| 5622 |
+
"version": "10.5.0",
|
| 5623 |
+
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
| 5624 |
+
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
| 5625 |
"license": "ISC",
|
| 5626 |
"dependencies": {
|
| 5627 |
"foreground-child": "^3.1.0",
|
|
|
|
| 6191 |
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
| 6192 |
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
| 6193 |
"license": "MIT",
|
|
|
|
| 6194 |
"bin": {
|
| 6195 |
"jiti": "bin/jiti.js"
|
| 6196 |
}
|
|
|
|
| 8031 |
}
|
| 8032 |
],
|
| 8033 |
"license": "MIT",
|
|
|
|
| 8034 |
"dependencies": {
|
| 8035 |
"nanoid": "^3.3.11",
|
| 8036 |
"picocolors": "^1.1.1",
|
|
|
|
| 8342 |
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
| 8343 |
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
| 8344 |
"license": "MIT",
|
|
|
|
| 8345 |
"engines": {
|
| 8346 |
"node": ">=0.10.0"
|
| 8347 |
}
|
|
|
|
| 8351 |
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
| 8352 |
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
| 8353 |
"license": "MIT",
|
|
|
|
| 8354 |
"dependencies": {
|
| 8355 |
"scheduler": "^0.27.0"
|
| 8356 |
},
|
|
|
|
| 9393 |
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
| 9394 |
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
| 9395 |
"license": "MIT",
|
|
|
|
| 9396 |
"dependencies": {
|
| 9397 |
"@alloc/quick-lru": "^5.2.0",
|
| 9398 |
"arg": "^5.0.2",
|
|
|
|
| 9524 |
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 9525 |
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 9526 |
"license": "MIT",
|
|
|
|
| 9527 |
"engines": {
|
| 9528 |
"node": ">=12"
|
| 9529 |
},
|
|
|
|
| 9703 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 9704 |
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 9705 |
"license": "Apache-2.0",
|
|
|
|
| 9706 |
"bin": {
|
| 9707 |
"tsc": "bin/tsc",
|
| 9708 |
"tsserver": "bin/tsserver"
|
|
|
|
| 10105 |
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 10106 |
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 10107 |
"license": "MIT",
|
|
|
|
| 10108 |
"engines": {
|
| 10109 |
"node": ">=12"
|
| 10110 |
},
|
|
|
|
| 10295 |
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
| 10296 |
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
| 10297 |
"license": "ISC",
|
|
|
|
| 10298 |
"bin": {
|
| 10299 |
"yaml": "bin.mjs"
|
| 10300 |
},
|
|
|
|
| 10421 |
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
| 10422 |
"dev": true,
|
| 10423 |
"license": "MIT",
|
|
|
|
| 10424 |
"funding": {
|
| 10425 |
"url": "https://github.com/sponsors/colinhacks"
|
| 10426 |
}
|
frontend/vite.config.ts
CHANGED
|
@@ -13,7 +13,7 @@ export default defineConfig({
|
|
| 13 |
server: {
|
| 14 |
proxy: {
|
| 15 |
'/api': {
|
| 16 |
-
target: 'http://localhost:
|
| 17 |
changeOrigin: true,
|
| 18 |
},
|
| 19 |
},
|
|
|
|
| 13 |
server: {
|
| 14 |
proxy: {
|
| 15 |
'/api': {
|
| 16 |
+
target: 'http://localhost:8001',
|
| 17 |
changeOrigin: true,
|
| 18 |
},
|
| 19 |
},
|
mcp_init_request.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"jsonrpc": "2.0",
|
| 3 |
+
"id": 1,
|
| 4 |
+
"method": "initialize",
|
| 5 |
+
"params": {
|
| 6 |
+
"protocolVersion": "2024-11-05",
|
| 7 |
+
"capabilities": {},
|
| 8 |
+
"clientInfo": {
|
| 9 |
+
"name": "test-client",
|
| 10 |
+
"version": "1.0.0"
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
|
start-dev.sh
CHANGED
|
@@ -37,12 +37,12 @@ echo -e "${GREEN}Starting backend server...${NC}"
|
|
| 37 |
cd "$BACKEND_DIR"
|
| 38 |
JWT_SECRET_KEY="local-dev-secret-key-123" \
|
| 39 |
VAULT_BASE_PATH="$PROJECT_ROOT/data/vaults" \
|
| 40 |
-
.venv/bin/uvicorn src.api.main:app --host 0.0.0.0 --port
|
| 41 |
BACKEND_PID=$!
|
| 42 |
echo $BACKEND_PID > "$BACKEND_PID_FILE"
|
| 43 |
echo -e "${GREEN}β Backend started (PID: $BACKEND_PID)${NC}"
|
| 44 |
echo " Logs: $PROJECT_ROOT/backend.log"
|
| 45 |
-
echo " URL: http://localhost:
|
| 46 |
|
| 47 |
# Wait a moment for backend to start
|
| 48 |
sleep 2
|
|
@@ -63,7 +63,7 @@ echo "Development servers are running!"
|
|
| 63 |
echo "=================================================="
|
| 64 |
echo -e "${NC}"
|
| 65 |
echo "Frontend: http://localhost:5173"
|
| 66 |
-
echo "Backend: http://localhost:
|
| 67 |
echo ""
|
| 68 |
echo "To stop servers, run: ./stop-dev.sh"
|
| 69 |
echo "To view logs, run: tail -f backend.log frontend.log"
|
|
|
|
| 37 |
cd "$BACKEND_DIR"
|
| 38 |
JWT_SECRET_KEY="local-dev-secret-key-123" \
|
| 39 |
VAULT_BASE_PATH="$PROJECT_ROOT/data/vaults" \
|
| 40 |
+
.venv/bin/uvicorn src.api.main:app --host 0.0.0.0 --port 8001 --reload > "$PROJECT_ROOT/backend.log" 2>&1 &
|
| 41 |
BACKEND_PID=$!
|
| 42 |
echo $BACKEND_PID > "$BACKEND_PID_FILE"
|
| 43 |
echo -e "${GREEN}β Backend started (PID: $BACKEND_PID)${NC}"
|
| 44 |
echo " Logs: $PROJECT_ROOT/backend.log"
|
| 45 |
+
echo " URL: http://localhost:8001"
|
| 46 |
|
| 47 |
# Wait a moment for backend to start
|
| 48 |
sleep 2
|
|
|
|
| 63 |
echo "=================================================="
|
| 64 |
echo -e "${NC}"
|
| 65 |
echo "Frontend: http://localhost:5173"
|
| 66 |
+
echo "Backend: http://localhost:8001"
|
| 67 |
echo ""
|
| 68 |
echo "To stop servers, run: ./stop-dev.sh"
|
| 69 |
echo "To view logs, run: tail -f backend.log frontend.log"
|
status-dev.sh
CHANGED
|
@@ -19,7 +19,7 @@ if [ -f "$BACKEND_PID_FILE" ]; then
|
|
| 19 |
BACKEND_PID=$(cat "$BACKEND_PID_FILE")
|
| 20 |
if ps -p $BACKEND_PID > /dev/null 2>&1; then
|
| 21 |
echo -e "${GREEN}β Backend: RUNNING${NC} (PID: $BACKEND_PID)"
|
| 22 |
-
echo " URL: http://localhost:
|
| 23 |
else
|
| 24 |
echo -e "${RED}β Backend: NOT RUNNING${NC} (stale PID: $BACKEND_PID)"
|
| 25 |
fi
|
|
|
|
| 19 |
BACKEND_PID=$(cat "$BACKEND_PID_FILE")
|
| 20 |
if ps -p $BACKEND_PID > /dev/null 2>&1; then
|
| 21 |
echo -e "${GREEN}β Backend: RUNNING${NC} (PID: $BACKEND_PID)"
|
| 22 |
+
echo " URL: http://localhost:8001"
|
| 23 |
else
|
| 24 |
echo -e "${RED}β Backend: NOT RUNNING${NC} (stale PID: $BACKEND_PID)"
|
| 25 |
fi
|