Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ node_modules/
.venv/
venv/
dist/
/.tmp-playwright
*.err
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from routes.cart import router as cart_router
from routes.chat import router as chat_router
from routes.image_proxy import router as image_router
from routes.mcp_debug import router as mcp_debug_router
from routes.meta import router as meta_router
from routes.ws import router as ws_router

Expand Down Expand Up @@ -47,6 +48,7 @@ async def lifespan(app: FastAPI):
app.include_router(ws_router)
app.include_router(image_router)
app.include_router(meta_router)
app.include_router(mcp_debug_router)


@app.get('/health')
Expand Down
61 changes: 61 additions & 0 deletions backend/routes/mcp_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import annotations

import json

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field

from mcp.client import mcp_client

router = APIRouter(prefix="/api/mcp", tags=["mcp-debug"])


class MCPDebugRequest(BaseModel):
tool: str = Field(min_length=1)
params: dict = Field(default_factory=dict)


def _validate_debug_tool_name(name: str) -> str:
clean = name.strip()
if not clean.startswith("kapruka_"):
raise HTTPException(status_code=400, detail="Only kapruka_* tools are allowed")
return clean


def _validate_debug_params(params: dict) -> dict:
try:
payload = json.dumps(params)
except TypeError as exc:
raise HTTPException(status_code=400, detail=f"Params must be JSON-serializable: {exc}") from exc
if len(payload) > 5000:
raise HTTPException(status_code=400, detail="Params payload is too large")
return params


@router.get("/debug")
async def mcp_debug_meta():
try:
tools = await mcp_client.list_tools()
return {
"connected": True,
"tool_count": len(tools),
"tools": [tool.get("name", "") for tool in tools],
}
except Exception as exc:
return {
"connected": False,
"tool_count": 0,
"tools": [],
"error": str(exc),
}


@router.post("/debug")
async def mcp_debug_call(req: MCPDebugRequest):
tool = _validate_debug_tool_name(req.tool)
params = _validate_debug_params(req.params)
result = await mcp_client.call_tool(tool, params)
return {
"tool": tool,
"result": result,
}
20 changes: 20 additions & 0 deletions backend/tests/test_mcp_debug_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import unittest
from fastapi import HTTPException

from routes.mcp_debug import _validate_debug_params, _validate_debug_tool_name


class MCPDebugRouteTest(unittest.TestCase):
def test_allows_only_kapruka_tools(self):
self.assertEqual(_validate_debug_tool_name("kapruka_search_products"), "kapruka_search_products")
with self.assertRaises(HTTPException):
_validate_debug_tool_name("search_products")

def test_rejects_oversized_payloads(self):
self.assertEqual(_validate_debug_params({"q": "cake"}), {"q": "cake"})
with self.assertRaises(HTTPException):
_validate_debug_params({"blob": "x" * 6000})


if __name__ == "__main__":
unittest.main()
33 changes: 23 additions & 10 deletions docs/feature-checklists/07-reliability-and-debug-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,33 @@ Finish the candidate-inspired hardening work: predictable small-model defaults,

## Checklist

- [ ] Review candidate MCP route and rate-limiting/debug patterns
- [ ] Confirm which reliability work is already shipped versus still missing
- [ ] Add or refine a minimal MCP debug route if still valuable
- [ ] Add lightweight request hardening only where it helps the demo app
- [ ] Keep the solution small and avoid unnecessary platform complexity
- [ ] Verify provider fallback behavior against live or simulated failures
- [ ] Run backend tests relevant to provider selection and fallback
- [ ] Run `npm run lint`
- [ ] Run `npm run build`
- [ ] Verify the flow in a live browser session
- [x] Review candidate MCP route and rate-limiting/debug patterns
- [x] Confirm which reliability work is already shipped versus still missing
- [x] Add or refine a minimal MCP debug route if still valuable
- [x] Add lightweight request hardening only where it helps the demo app
- [x] Keep the solution small and avoid unnecessary platform complexity
- [x] Verify provider fallback behavior against live or simulated failures
- [x] Run backend tests relevant to provider selection and fallback
- [x] Run `npm run lint`
- [x] Run `npm run build`
- [x] Verify the flow in a live browser session
- [ ] Commit, push branch, and open draft PR

## Notes

- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/app/api/mcp/route.js`
- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/rate-limiter.js`
- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/mcp/client.js`
- Existing provider fallback and small-model defaults were already shipped earlier, so this iteration only adds the missing MCP debug surface.
- The debug surface is intentionally tiny: `GET /api/mcp/debug` for tool visibility and `POST /api/mcp/debug` for explicit `kapruka_*` tool calls only.
- Lightweight hardening added:
- only `kapruka_*` tools are allowed
- params must be JSON-serializable
- params payload is capped at 5 KB
- Verification evidence:
- `python -m unittest tests.test_mcp_debug_route tests.test_provider_selection tests.test_mcp_client tests.test_chat_route`
- `npm.cmd run lint`
- `npm.cmd run build`
- live route checks against local backend:
- `GET /api/mcp/debug` responded successfully
- invalid `POST /api/mcp/debug` with non-`kapruka_*` tool returned `400`