Wothmag07 commited on
Commit
3777688
Β·
1 Parent(s): d497a35

Local MCP check

Browse files
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 8000 --reload
118
  ```
119
 
120
- Backend will be available at: `http://localhost:8000`
121
 
122
- API docs (Swagger): `http://localhost:8000/docs`
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 8000
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
- def main():
2
- print("Hello from backend!")
3
 
 
4
 
5
  if __name__ == "__main__":
6
- main()
 
 
 
 
 
 
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 = "backend"
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 fastapi.routing import ASGIRoute
 
 
 
 
14
  from starlette.responses import Response
15
 
16
- from fastmcp.server.streamable_http import StreamableHTTPSessionManager
 
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=["http://localhost:5173", "http://localhost:3000", "https://huggingface.co"],
 
 
 
 
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("/assets", StaticFiles(directory=str(frontend_dist / "assets")), name="assets")
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 full_path.startswith(("api/", "auth/", "health", "mcp")):
 
 
 
 
 
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 = [state for state, ts in oauth_states.items() if now - ts > OAUTH_STATE_TTL_SECONDS]
 
 
 
 
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(None, description="State parameter for CSRF protection"),
 
 
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
- status_code=501,
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
- jwt_token = jwt.encode(payload, config.jwt_secret_key, algorithm="HS256")
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(name="list_notes", description="List notes in the vault (optionally scoped to a folder).")
 
 
 
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(..., description="Relative '.md' path ≀256 chars (no '..' or '\\')."),
 
 
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(..., description="Relative '.md' path ≀256 chars (no '..' or '\\')."),
 
 
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(..., description="Relative '.md' path ≀256 chars (no '..' or '\\')."),
 
 
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(name="get_backlinks", description="List notes that reference the target note.")
 
 
235
  def get_backlinks(
236
- path: str = Field(..., description="Relative '.md' path ≀256 chars (no '..' or '\\')."),
 
 
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
- mcp.run(transport="stdio")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = 518000, upload-time = "2025-10-22T22:22:11.326Z" },
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:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
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.4.5",
5634
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
5635
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
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:8000',
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 8000 --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:8000"
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:8000"
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:8000"
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