Spaces:
Sleeping
Sleeping
google-labs-jules[bot]
commited on
Commit
·
c322df2
1
Parent(s):
0d26b0e
Refactor repo for Hugging Face Spaces compatibility
Browse files- Created root `app.py` as entry point initializing DB and launching Gradio with MCP.
- Updated `README.md` with HF Spaces YAML header and detailed instructions.
- Refactored `src/alert_mcp_server/tools.py` to use direct DB calls via `src.alert_mcp` instead of `httpx` HTTP requests, simplifying deployment.
- Updated `src/alert_mcp_server/app.py` to use relative imports.
- Updated tests to mock `mcp_tools` and verify Pydantic V2 compatibility.
- Created `requirements.txt` with pinned dependencies (`pydantic>=2.0`, `gradio[mcp]`).
- Ensured `__init__.py` files exist for package recognition.
- README.md +91 -87
- app.py +17 -0
- requirements.txt +8 -0
- src/__init__.py +0 -0
- src/alert_mcp_server/__init__.py +0 -0
- src/alert_mcp_server/app.py +1 -1
- src/alert_mcp_server/tests/test_alert_mcp_server.py +41 -83
- src/alert_mcp_server/tools.py +53 -82
README.md
CHANGED
|
@@ -1,127 +1,131 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 6 |
|
| 7 |
-
|
| 8 |
-
- **Model Context Protocol (MCP)** for standardized tool interfaces.
|
| 9 |
-
- **LangGraph** for agentic workflows (sweeps, Q&A).
|
| 10 |
-
- **Gradio** for the user interface.
|
| 11 |
-
- **FastAPI & SQLite** for backend services and data persistence.
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
3. **alert_mcp** (This Repository): Alert logging, listing, and resolution.
|
| 19 |
|
| 20 |
-
|
| 21 |
|
| 22 |
-
|
| 23 |
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
| 27 |
|
| 28 |
-
|
| 29 |
-
2. **`src/alert_mcp_server`**: A Gradio-based MCP server. It acts as a frontend/wrapper that exposes the alert capabilities as MCP tools (via SSE) and provides a simple web UI for testing and interaction.
|
| 30 |
|
| 31 |
-
|
| 32 |
-
-
|
| 33 |
-
- **View Alerts**: Query open alerts, filtered by provider or severity.
|
| 34 |
-
- **Resolve Alert**: Mark alerts as resolved with a note.
|
| 35 |
-
- **Summarize**: Get a breakdown of alerts by severity.
|
| 36 |
-
- **MCP Support**: Exposes these functions as MCP tools for agents to use.
|
| 37 |
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
| 41 |
|
| 42 |
-
-
|
| 43 |
-
-
|
| 44 |
|
| 45 |
-
##
|
| 46 |
|
| 47 |
-
|
| 48 |
-
```bash
|
| 49 |
-
git clone <repo-url>
|
| 50 |
-
cd <repo-dir>
|
| 51 |
-
```
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
Or using pip:
|
| 58 |
-
```bash
|
| 59 |
-
pip install .
|
| 60 |
-
```
|
| 61 |
|
| 62 |
-
|
| 63 |
|
| 64 |
-
|
| 65 |
|
| 66 |
-
```
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
```
|
| 71 |
|
| 72 |
-
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
The backend service manages the database and API.
|
| 77 |
|
| 78 |
```bash
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
```
|
| 81 |
-
This will start the FastAPI server at `http://localhost:8000`. It will automatically create the SQLite database at `DB_FILE_PATH` (defaulting to `./credentialwatch.db` or similar if not set).
|
| 82 |
|
| 83 |
-
|
| 84 |
|
| 85 |
-
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
|
| 92 |
-
|
| 93 |
|
| 94 |
-
|
| 95 |
-
-
|
| 96 |
-
-
|
| 97 |
-
- `mark_alert_resolved(...)`
|
| 98 |
|
| 99 |
-
|
| 100 |
|
| 101 |
-
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
|
| 109 |
```
|
| 110 |
.
|
|
|
|
|
|
|
| 111 |
├── src/
|
| 112 |
-
│ ├── alert_mcp/ #
|
| 113 |
-
│
|
| 114 |
-
|
| 115 |
-
│ │ ├── db.py # DB connection logic
|
| 116 |
-
│ │ └── ...
|
| 117 |
-
│ └── alert_mcp_server/ # Gradio UI & MCP Tool Wrapper
|
| 118 |
-
│ ├── main.py # Gradio app entrypoint
|
| 119 |
-
│ ├── tools.py # Client logic to talk to backend
|
| 120 |
-
│ └── ...
|
| 121 |
-
├── tests/ # Test suite
|
| 122 |
-
└── pyproject.toml # Dependencies
|
| 123 |
```
|
| 124 |
-
|
| 125 |
-
## License
|
| 126 |
-
|
| 127 |
-
MIT
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: CredentialWatch Alert MCP
|
| 3 |
+
emoji: 🩺
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
python_version: 3.11
|
| 8 |
+
sdk_version: 6.0.0
|
| 9 |
+
app_file: app.py
|
| 10 |
+
fullWidth: true
|
| 11 |
+
short_description: "Gradio MCP server for healthcare credential alerts."
|
| 12 |
+
tags:
|
| 13 |
+
- mcp
|
| 14 |
+
- gradio
|
| 15 |
+
- tools
|
| 16 |
+
- healthcare
|
| 17 |
+
pinned: false
|
| 18 |
+
---
|
| 19 |
|
| 20 |
+
# CredentialWatch Alert MCP Server
|
| 21 |
|
| 22 |
+
Agent-ready Gradio Space that exposes healthcare credential alerts tools (log, view, resolve) over **Model Context Protocol (MCP)**.
|
| 23 |
|
| 24 |
+
## Hugging Face Space
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
This repository is designed to run as a **Gradio Space**.
|
| 27 |
|
| 28 |
+
- SDK: Gradio (`sdk: gradio` in the README header)
|
| 29 |
+
- Entry file: `app.py` (set via `app_file` in the YAML header)
|
| 30 |
+
- Python: 3.11 (pinned with `python_version`)
|
|
|
|
| 31 |
|
| 32 |
+
When you push this repo to a Space with SDK = **Gradio**, the UI and the MCP server will be started automatically.
|
| 33 |
|
| 34 |
+
## MCP Server
|
| 35 |
|
| 36 |
+
This Space exposes its tools via **Model Context Protocol (MCP)** using Gradio.
|
| 37 |
|
| 38 |
+
### How MCP is enabled
|
| 39 |
|
| 40 |
+
In `app.py` we:
|
|
|
|
| 41 |
|
| 42 |
+
- initialize the database
|
| 43 |
+
- launch the app with MCP support: `demo.launch(mcp_server=True)`
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
### MCP endpoints
|
| 46 |
|
| 47 |
+
When the Space is running, Gradio exposes:
|
| 48 |
|
| 49 |
+
- **MCP SSE endpoint**: `https://<space-host>/gradio_api/mcp/sse`
|
| 50 |
+
- **MCP schema**: `https://<space-host>/gradio_api/mcp/schema`
|
| 51 |
|
| 52 |
+
## Using this Space from an MCP client
|
| 53 |
|
| 54 |
+
### Easiest: Hugging Face MCP Server (no manual config)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
+
1. Go to your HF **MCP settings**: https://huggingface.co/settings/mcp
|
| 57 |
+
2. Add this Space under **Spaces Tools** (look for the MCP badge on the Space).
|
| 58 |
+
3. Restart your MCP client (VS Code, Cursor, Claude Code, etc.).
|
| 59 |
+
4. The tools from this Space will appear as MCP tools and can be called directly.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
### Manual config (generic MCP client using mcp-remote)
|
| 62 |
|
| 63 |
+
If your MCP client uses a JSON config, you can point it to the SSE endpoint via `mcp-remote`:
|
| 64 |
|
| 65 |
+
```jsonc
|
| 66 |
+
{
|
| 67 |
+
"mcpServers": {
|
| 68 |
+
"credentialwatch-alert": {
|
| 69 |
+
"command": "npx",
|
| 70 |
+
"args": [
|
| 71 |
+
"mcp-remote",
|
| 72 |
+
"https://<space-host>/gradio_api/mcp/sse"
|
| 73 |
+
]
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
```
|
| 78 |
|
| 79 |
+
Replace `<space-host>` with the full URL of your Space.
|
| 80 |
|
| 81 |
+
## Local development
|
|
|
|
|
|
|
| 82 |
|
| 83 |
```bash
|
| 84 |
+
# 1. Install deps
|
| 85 |
+
uv pip install -r requirements.txt
|
| 86 |
+
|
| 87 |
+
# 2. Run locally
|
| 88 |
+
uv run python app.py
|
| 89 |
```
|
|
|
|
| 90 |
|
| 91 |
+
The local server will be available at http://127.0.0.1:7860, and MCP at http://127.0.0.1:7860/gradio_api/mcp/sse.
|
| 92 |
|
| 93 |
+
## Deploying to Hugging Face Spaces
|
| 94 |
|
| 95 |
+
1. Create a new Space with SDK = **Gradio**.
|
| 96 |
+
2. Push this repo to the Space (Git or `huggingface_hub`).
|
| 97 |
+
3. Ensure the YAML header in `README.md` is present and correct.
|
| 98 |
+
4. Wait for the Space to build and start — it should show an **MCP badge** automatically.
|
| 99 |
|
| 100 |
+
## Troubleshooting
|
| 101 |
|
| 102 |
+
- If the Space shows **Configuration error**, verify `sdk`, `app_file`, and `python_version` in the YAML header.
|
| 103 |
+
- If the **MCP badge** doesn't appear, confirm `demo.launch(mcp_server=True)` is called in `app.py`.
|
| 104 |
+
- Ensure `README.md` is at the root and not tracked by LFS.
|
|
|
|
| 105 |
|
| 106 |
+
---
|
| 107 |
|
| 108 |
+
## Original Documentation
|
| 109 |
|
| 110 |
+
### Overview
|
| 111 |
+
|
| 112 |
+
CredentialWatch is a demo product for the **Hugging Face "MCP 1st Birthday / Gradio Agents Hackathon"**. It is designed to act as a unified, queryable view of provider credentials and a proactive alerting system.
|
| 113 |
+
|
| 114 |
+
### Features
|
| 115 |
+
- **Log Alert**: Create new alerts with severity levels (info, warning, critical).
|
| 116 |
+
- **View Alerts**: Query open alerts, filtered by provider or severity.
|
| 117 |
+
- **Resolve Alert**: Mark alerts as resolved with a note.
|
| 118 |
+
- **Summarize**: Get a breakdown of alerts by severity.
|
| 119 |
+
- **MCP Support**: Exposes these functions as MCP tools for agents to use.
|
| 120 |
|
| 121 |
+
### Project Structure
|
| 122 |
|
| 123 |
```
|
| 124 |
.
|
| 125 |
+
├── app.py # Entry point for HF Spaces
|
| 126 |
+
├── requirements.txt # Dependencies
|
| 127 |
├── src/
|
| 128 |
+
│ ├── alert_mcp/ # Backend logic & DB Models
|
| 129 |
+
│ └── alert_mcp_server/ # Gradio UI & Tool Wrappers
|
| 130 |
+
└── ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
```
|
|
|
|
|
|
|
|
|
|
|
|
app.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Ensure the root directory is in the python path
|
| 5 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 6 |
+
|
| 7 |
+
from src.alert_mcp.db import init_db
|
| 8 |
+
from src.alert_mcp_server.app import create_demo
|
| 9 |
+
|
| 10 |
+
# Initialize the database
|
| 11 |
+
init_db()
|
| 12 |
+
|
| 13 |
+
# Create and launch the demo
|
| 14 |
+
demo = create_demo()
|
| 15 |
+
|
| 16 |
+
if __name__ == "__main__":
|
| 17 |
+
demo.launch(mcp_server=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio[mcp]
|
| 2 |
+
pydantic>=2.0
|
| 3 |
+
python-dotenv
|
| 4 |
+
mcp
|
| 5 |
+
sqlalchemy
|
| 6 |
+
fastapi
|
| 7 |
+
uvicorn
|
| 8 |
+
httpx
|
src/__init__.py
ADDED
|
File without changes
|
src/alert_mcp_server/__init__.py
ADDED
|
File without changes
|
src/alert_mcp_server/app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
from tools import log_alert, get_open_alerts, mark_alert_resolved, summarize_alerts
|
| 3 |
|
| 4 |
# Define the Gradio interface
|
| 5 |
# We can use a TabbedInterface to organize the tools for the UI,
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
from .tools import log_alert, get_open_alerts, mark_alert_resolved, summarize_alerts
|
| 3 |
|
| 4 |
# Define the Gradio interface
|
| 5 |
# We can use a TabbedInterface to organize the tools for the UI,
|
src/alert_mcp_server/tests/test_alert_mcp_server.py
CHANGED
|
@@ -1,104 +1,62 @@
|
|
| 1 |
-
|
| 2 |
import pytest
|
| 3 |
-
import
|
| 4 |
-
from unittest.mock import AsyncMock, MagicMock, patch
|
| 5 |
from src.alert_mcp_server.tools import log_alert, get_open_alerts, mark_alert_resolved, summarize_alerts
|
| 6 |
|
| 7 |
-
@pytest.
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
"message": "Test alert",
|
| 14 |
-
"created_at": "2023-10-27T10:00:00"
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
# We use new_callable=AsyncMock for the client.post method because it is awaited.
|
| 18 |
-
# However, the return value (the response object) should be a synchronous Mock (MagicMock)
|
| 19 |
-
# because methods like .json() and .raise_for_status() are synchronous on the Response object.
|
| 20 |
-
with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
|
| 21 |
-
mock_response_obj = MagicMock()
|
| 22 |
-
mock_response_obj.status_code = 200
|
| 23 |
-
mock_response_obj.json.return_value = mock_response
|
| 24 |
-
mock_response_obj.raise_for_status.return_value = None
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
|
|
|
|
| 29 |
provider_id=1,
|
| 30 |
severity="info",
|
| 31 |
window_days=30,
|
| 32 |
message="Test alert"
|
| 33 |
)
|
| 34 |
|
| 35 |
-
assert result ==
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
assert kwargs["json"]["severity"] == "info"
|
| 40 |
-
|
| 41 |
-
@pytest.mark.asyncio
|
| 42 |
-
async def test_get_open_alerts():
|
| 43 |
-
mock_response = [
|
| 44 |
-
{
|
| 45 |
-
"id": 1,
|
| 46 |
-
"provider_id": 1,
|
| 47 |
-
"severity": "critical",
|
| 48 |
-
"message": "Critical alert",
|
| 49 |
-
"created_at": "2023-10-27T10:00:00"
|
| 50 |
-
}
|
| 51 |
-
]
|
| 52 |
-
|
| 53 |
-
with patch("httpx.AsyncClient.get", new_callable=AsyncMock) as mock_get:
|
| 54 |
-
mock_response_obj = MagicMock()
|
| 55 |
-
mock_response_obj.status_code = 200
|
| 56 |
-
mock_response_obj.json.return_value = mock_response
|
| 57 |
-
mock_response_obj.raise_for_status.return_value = None
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
|
|
|
|
| 62 |
|
| 63 |
-
assert result ==
|
|
|
|
| 64 |
mock_get.assert_called_once()
|
| 65 |
-
|
| 66 |
-
assert kwargs["params"]["provider_id"] == 1
|
| 67 |
-
|
| 68 |
-
@pytest.mark.asyncio
|
| 69 |
-
async def test_mark_alert_resolved():
|
| 70 |
-
mock_response = {
|
| 71 |
-
"id": 1,
|
| 72 |
-
"resolved_at": "2023-10-27T12:00:00",
|
| 73 |
-
"resolution_note": "Fixed"
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
|
| 77 |
-
mock_response_obj = MagicMock()
|
| 78 |
-
mock_response_obj.status_code = 200
|
| 79 |
-
mock_response_obj.json.return_value = mock_response
|
| 80 |
-
mock_response_obj.raise_for_status.return_value = None
|
| 81 |
-
|
| 82 |
-
mock_post.return_value = mock_response_obj
|
| 83 |
-
|
| 84 |
-
result = await mark_alert_resolved(alert_id=1, resolution_note="Fixed")
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
mock_response = {"info": 5, "warning": 2, "critical": 1}
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
mock_response_obj.json.return_value = mock_response
|
| 97 |
-
mock_response_obj.raise_for_status.return_value = None
|
| 98 |
|
| 99 |
-
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
|
|
|
|
| 102 |
|
| 103 |
-
assert result ==
|
| 104 |
-
|
|
|
|
|
|
|
|
|
| 1 |
import pytest
|
| 2 |
+
from unittest.mock import MagicMock, patch
|
|
|
|
| 3 |
from src.alert_mcp_server.tools import log_alert, get_open_alerts, mark_alert_resolved, summarize_alerts
|
| 4 |
|
| 5 |
+
@pytest.fixture
|
| 6 |
+
def mock_db_session():
|
| 7 |
+
with patch("src.alert_mcp_server.tools.get_db") as mock_get_db:
|
| 8 |
+
mock_session = MagicMock()
|
| 9 |
+
mock_get_db.return_value = iter([mock_session])
|
| 10 |
+
yield mock_session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
def test_log_alert(mock_db_session):
|
| 13 |
+
mock_alert_read = MagicMock()
|
| 14 |
+
# Mock model_dump return value
|
| 15 |
+
mock_alert_read.model_dump.return_value = {"id": 1, "provider_id": 1, "severity": "info", "message": "Test alert"}
|
| 16 |
|
| 17 |
+
with patch("src.alert_mcp.mcp_tools.log_alert", return_value=mock_alert_read) as mock_log:
|
| 18 |
+
result = log_alert(
|
| 19 |
provider_id=1,
|
| 20 |
severity="info",
|
| 21 |
window_days=30,
|
| 22 |
message="Test alert"
|
| 23 |
)
|
| 24 |
|
| 25 |
+
assert result["id"] == 1
|
| 26 |
+
mock_log.assert_called_once()
|
| 27 |
+
# Verify model_dump was called with mode='json'
|
| 28 |
+
mock_alert_read.model_dump.assert_called_with(mode='json')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
def test_get_open_alerts(mock_db_session):
|
| 31 |
+
mock_alert_read = MagicMock()
|
| 32 |
+
mock_alert_read.model_dump.return_value = {"id": 1, "provider_id": 1, "severity": "critical"}
|
| 33 |
|
| 34 |
+
with patch("src.alert_mcp.mcp_tools.get_open_alerts", return_value=[mock_alert_read]) as mock_get:
|
| 35 |
+
result = get_open_alerts(provider_id=1)
|
| 36 |
|
| 37 |
+
assert len(result) == 1
|
| 38 |
+
assert result[0]["id"] == 1
|
| 39 |
mock_get.assert_called_once()
|
| 40 |
+
mock_alert_read.model_dump.assert_called_with(mode='json')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
def test_mark_alert_resolved(mock_db_session):
|
| 43 |
+
mock_alert_read = MagicMock()
|
| 44 |
+
mock_alert_read.model_dump.return_value = {"id": 1, "resolved_at": "2023-10-27"}
|
| 45 |
|
| 46 |
+
with patch("src.alert_mcp.mcp_tools.mark_alert_resolved", return_value=mock_alert_read) as mock_mark:
|
| 47 |
+
result = mark_alert_resolved(alert_id=1, resolution_note="Fixed")
|
|
|
|
| 48 |
|
| 49 |
+
assert result["id"] == 1
|
| 50 |
+
mock_mark.assert_called_once()
|
| 51 |
+
mock_alert_read.model_dump.assert_called_with(mode='json')
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
def test_summarize_alerts(mock_db_session):
|
| 54 |
+
mock_summary = MagicMock()
|
| 55 |
+
mock_summary.model_dump.return_value = {"total_alerts": 10, "by_severity": {"info": 5}}
|
| 56 |
|
| 57 |
+
with patch("src.alert_mcp.mcp_tools.summarize_alerts", return_value=mock_summary) as mock_sum:
|
| 58 |
+
result = summarize_alerts(window_days=7)
|
| 59 |
|
| 60 |
+
assert result["total_alerts"] == 10
|
| 61 |
+
mock_sum.assert_called_once()
|
| 62 |
+
mock_summary.model_dump.assert_called_with(mode='json')
|
src/alert_mcp_server/tools.py
CHANGED
|
@@ -1,15 +1,12 @@
|
|
| 1 |
-
import
|
| 2 |
from typing import List, Optional, Dict, Any
|
| 3 |
-
from
|
| 4 |
-
from
|
| 5 |
|
| 6 |
-
# We
|
| 7 |
-
#
|
| 8 |
-
# Actually, httpx is great for async, but standard requests is often used.
|
| 9 |
-
# Let's stick to httpx but use it synchronously or asynchronously depending on how we define the tools.
|
| 10 |
-
# Gradio tools can be async def.
|
| 11 |
|
| 12 |
-
|
| 13 |
provider_id: int,
|
| 14 |
severity: str,
|
| 15 |
window_days: int,
|
|
@@ -28,32 +25,26 @@ async def log_alert(
|
|
| 28 |
credential_id: Optional credential ID.
|
| 29 |
channel: Notification channel (default: "ui").
|
| 30 |
"""
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
|
| 38 |
-
"provider_id": provider_id,
|
| 39 |
-
"credential_id": credential_id,
|
| 40 |
-
"severity": severity,
|
| 41 |
-
"window_days": window_days,
|
| 42 |
-
"message": message,
|
| 43 |
-
"channel": channel
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
async with httpx.AsyncClient(base_url=ALERT_API_BASE_URL) as client:
|
| 47 |
-
try:
|
| 48 |
-
response = await client.post("/alerts", json=payload)
|
| 49 |
-
response.raise_for_status()
|
| 50 |
-
return response.json()
|
| 51 |
-
except httpx.HTTPStatusError as e:
|
| 52 |
-
return {"error": f"HTTP error occurred: {e.response.status_code} - {e.response.text}"}
|
| 53 |
-
except Exception as e:
|
| 54 |
-
return {"error": f"An error occurred: {str(e)}"}
|
| 55 |
-
|
| 56 |
-
async def get_open_alerts(
|
| 57 |
provider_id: Optional[int] = None,
|
| 58 |
severity: Optional[str] = None
|
| 59 |
) -> List[Dict[str, Any]]:
|
|
@@ -64,30 +55,16 @@ async def get_open_alerts(
|
|
| 64 |
provider_id: Optional filter by provider ID.
|
| 65 |
severity: Optional filter by severity.
|
| 66 |
"""
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
# I'll try POST as it's more flexible for filters usually, or standard GET with query params.
|
| 76 |
-
# Let's assume POST for filters body if complex, or GET with query params.
|
| 77 |
-
# I will assume GET with query params first as it's standard for 'get', but the instruction says "GET or POST".
|
| 78 |
-
# I will implement as GET with query params for now.
|
| 79 |
|
| 80 |
-
|
| 81 |
-
try:
|
| 82 |
-
response = await client.get("/alerts/open", params=params)
|
| 83 |
-
response.raise_for_status()
|
| 84 |
-
return response.json()
|
| 85 |
-
except httpx.HTTPStatusError as e:
|
| 86 |
-
return [{"error": f"HTTP error: {e.response.status_code}"}]
|
| 87 |
-
except Exception as e:
|
| 88 |
-
return [{"error": str(e)}]
|
| 89 |
-
|
| 90 |
-
async def mark_alert_resolved(
|
| 91 |
alert_id: int,
|
| 92 |
resolution_note: Optional[str] = None
|
| 93 |
) -> Dict[str, Any]:
|
|
@@ -98,35 +75,29 @@ async def mark_alert_resolved(
|
|
| 98 |
alert_id: The ID of the alert to resolve.
|
| 99 |
resolution_note: A note explaining the resolution.
|
| 100 |
"""
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
return {"error": str(e)}
|
| 112 |
|
| 113 |
-
|
| 114 |
"""
|
| 115 |
Get a summary of alerts (counts by severity).
|
| 116 |
|
| 117 |
Args:
|
| 118 |
window_days: Optional window in days to summarize over.
|
| 119 |
"""
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
return response.json()
|
| 129 |
-
except httpx.HTTPStatusError as e:
|
| 130 |
-
return {"error": f"HTTP error: {e.response.status_code}"}
|
| 131 |
-
except Exception as e:
|
| 132 |
-
return {"error": str(e)}
|
|
|
|
| 1 |
+
import json
|
| 2 |
from typing import List, Optional, Dict, Any
|
| 3 |
+
from src.alert_mcp.db import get_db
|
| 4 |
+
from src.alert_mcp import mcp_tools
|
| 5 |
|
| 6 |
+
# We use synchronous calls directly to the database logic
|
| 7 |
+
# This avoids the need for a separate backend server process in the Space.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
def log_alert(
|
| 10 |
provider_id: int,
|
| 11 |
severity: str,
|
| 12 |
window_days: int,
|
|
|
|
| 25 |
credential_id: Optional credential ID.
|
| 26 |
channel: Notification channel (default: "ui").
|
| 27 |
"""
|
| 28 |
+
db = next(get_db())
|
| 29 |
+
try:
|
| 30 |
+
alert = mcp_tools.log_alert(
|
| 31 |
+
db=db,
|
| 32 |
+
provider_id=provider_id,
|
| 33 |
+
credential_id=credential_id,
|
| 34 |
+
severity=severity,
|
| 35 |
+
window_days=window_days,
|
| 36 |
+
message=message,
|
| 37 |
+
channel=channel
|
| 38 |
+
)
|
| 39 |
+
return alert.model_dump(mode='json')
|
| 40 |
+
except ValueError as e:
|
| 41 |
+
return {"error": str(e)}
|
| 42 |
+
except Exception as e:
|
| 43 |
+
return {"error": f"An error occurred: {str(e)}"}
|
| 44 |
+
finally:
|
| 45 |
+
db.close()
|
| 46 |
|
| 47 |
+
def get_open_alerts(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
provider_id: Optional[int] = None,
|
| 49 |
severity: Optional[str] = None
|
| 50 |
) -> List[Dict[str, Any]]:
|
|
|
|
| 55 |
provider_id: Optional filter by provider ID.
|
| 56 |
severity: Optional filter by severity.
|
| 57 |
"""
|
| 58 |
+
db = next(get_db())
|
| 59 |
+
try:
|
| 60 |
+
alerts = mcp_tools.get_open_alerts(db=db, provider_id=provider_id, severity=severity)
|
| 61 |
+
return [a.model_dump(mode='json') for a in alerts]
|
| 62 |
+
except Exception as e:
|
| 63 |
+
return [{"error": str(e)}]
|
| 64 |
+
finally:
|
| 65 |
+
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
+
def mark_alert_resolved(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
alert_id: int,
|
| 69 |
resolution_note: Optional[str] = None
|
| 70 |
) -> Dict[str, Any]:
|
|
|
|
| 75 |
alert_id: The ID of the alert to resolve.
|
| 76 |
resolution_note: A note explaining the resolution.
|
| 77 |
"""
|
| 78 |
+
db = next(get_db())
|
| 79 |
+
try:
|
| 80 |
+
alert = mcp_tools.mark_alert_resolved(db=db, alert_id=alert_id, resolution_note=resolution_note)
|
| 81 |
+
return alert.model_dump(mode='json')
|
| 82 |
+
except ValueError as e:
|
| 83 |
+
return {"error": str(e)}
|
| 84 |
+
except Exception as e:
|
| 85 |
+
return {"error": str(e)}
|
| 86 |
+
finally:
|
| 87 |
+
db.close()
|
|
|
|
| 88 |
|
| 89 |
+
def summarize_alerts(window_days: Optional[int] = None) -> Dict[str, Any]:
|
| 90 |
"""
|
| 91 |
Get a summary of alerts (counts by severity).
|
| 92 |
|
| 93 |
Args:
|
| 94 |
window_days: Optional window in days to summarize over.
|
| 95 |
"""
|
| 96 |
+
db = next(get_db())
|
| 97 |
+
try:
|
| 98 |
+
summary = mcp_tools.summarize_alerts(db=db, window_days=window_days)
|
| 99 |
+
return summary.model_dump(mode='json')
|
| 100 |
+
except Exception as e:
|
| 101 |
+
return {"error": str(e)}
|
| 102 |
+
finally:
|
| 103 |
+
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|