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 CHANGED
@@ -1,127 +1,131 @@
1
- # CredentialWatch: Alert MCP Server
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- ## Overview
4
 
5
- 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 for expirations (state licenses, board certs, DEA/CDS, etc.) in a US-style healthcare setting.
6
 
7
- The system leverages:
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
- ### System Architecture
14
 
15
- The complete CredentialWatch system consists of three separate MCP servers:
16
- 1. **npi_mcp**: Read-only access to the public NPPES NPI Registry.
17
- 2. **cred_db_mcp**: Internal provider & credential database operations.
18
- 3. **alert_mcp** (This Repository): Alert logging, listing, and resolution.
19
 
20
- These servers interact with a **LangGraph Agent** and **Gradio UI** to provide a seamless experience for managing provider credentials and risks.
21
 
22
- ---
23
 
24
- ## This Repository: `alert_mcp` & `alert_mcp_server`
25
 
26
- This repository specifically houses the **Alert MCP** component, which includes:
27
 
28
- 1. **`src/alert_mcp`**: A FastAPI backend service that manages the `alerts` table in a SQLite database. It provides endpoints to log alerts, retrieve open alerts, and mark them as resolved.
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
- ### Features
32
- - **Log Alert**: Create new alerts with severity levels (info, warning, critical).
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
- ## Prerequisites
41
 
42
- - **Python 3.11**
43
- - **uv** (recommended for dependency management)
44
 
45
- ## Installation
46
 
47
- 1. **Clone the repository**:
48
- ```bash
49
- git clone <repo-url>
50
- cd <repo-dir>
51
- ```
52
 
53
- 2. **Install dependencies**:
54
- ```bash
55
- uv sync
56
- ```
57
- Or using pip:
58
- ```bash
59
- pip install .
60
- ```
61
 
62
- ## Configuration
63
 
64
- The system uses environment variables for configuration. Create a `.env` file or set them in your environment:
65
 
66
- ```bash
67
- # src/alert_mcp_server/config.py
68
- ALERT_API_BASE_URL=http://localhost:8000 # URL of the backend alert_mcp service
69
- DB_FILE_PATH=/data/credentialwatch.db # Path to SQLite DB (used by backend)
 
 
 
 
 
 
 
 
70
  ```
71
 
72
- ## Usage
73
 
74
- ### 1. Run the Backend (`alert_mcp`)
75
-
76
- The backend service manages the database and API.
77
 
78
  ```bash
79
- uv run uvicorn src.alert_mcp.main:app --reload --port 8000
 
 
 
 
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
- ### 2. Run the Gradio MCP Server (`alert_mcp_server`)
84
 
85
- The Gradio server connects to the backend and exposes the UI and MCP tools.
86
 
87
- ```bash
88
- uv run python -m src.alert_mcp_server.main
89
- ```
90
- This will launch the Gradio interface (usually at `http://localhost:7860`).
91
 
92
- ### Using with an MCP Client
93
 
94
- The `alert_mcp_server` exposes MCP tools via SSE (Server-Sent Events). An MCP-compatible agent (like the CredentialWatch LangGraph agent) can connect to this server to:
95
- - `log_alert(...)`
96
- - `get_open_alerts(...)`
97
- - `mark_alert_resolved(...)`
98
 
99
- ## Testing
100
 
101
- Run the test suite using `pytest`:
102
 
103
- ```bash
104
- uv run pytest
105
- ```
 
 
 
 
 
 
 
106
 
107
- ## Project Structure
108
 
109
  ```
110
  .
 
 
111
  ├── src/
112
- │ ├── alert_mcp/ # FastAPI Backend & DB Models
113
- ├── main.py # App entrypoint
114
- │ │ ├── models.py # SQLAlchemy models (Alert)
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 httpx
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.mark.asyncio
8
- async def test_log_alert():
9
- mock_response = {
10
- "id": 1,
11
- "provider_id": 1,
12
- "severity": "info",
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
- mock_post.return_value = mock_response_obj
 
 
 
27
 
28
- result = await log_alert(
 
29
  provider_id=1,
30
  severity="info",
31
  window_days=30,
32
  message="Test alert"
33
  )
34
 
35
- assert result == mock_response
36
- mock_post.assert_called_once()
37
- args, kwargs = mock_post.call_args
38
- assert kwargs["json"]["provider_id"] == 1
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
- mock_get.return_value = mock_response_obj
 
 
60
 
61
- result = await get_open_alerts(provider_id=1)
 
62
 
63
- assert result == mock_response
 
64
  mock_get.assert_called_once()
65
- args, kwargs = mock_get.call_args
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
- assert result == mock_response
87
- mock_post.assert_called_once()
 
88
 
89
- @pytest.mark.asyncio
90
- async def test_summarize_alerts():
91
- mock_response = {"info": 5, "warning": 2, "critical": 1}
92
 
93
- with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
94
- mock_response_obj = MagicMock()
95
- mock_response_obj.status_code = 200
96
- mock_response_obj.json.return_value = mock_response
97
- mock_response_obj.raise_for_status.return_value = None
98
 
99
- mock_post.return_value = mock_response_obj
 
 
100
 
101
- result = await summarize_alerts(window_days=7)
 
102
 
103
- assert result == mock_response
104
- mock_post.assert_called_once()
 
 
 
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 httpx
2
  from typing import List, Optional, Dict, Any
3
- from config import ALERT_API_BASE_URL
4
- from schemas import AlertCreate, AlertRead, AlertResolution
5
 
6
- # We'll use a synchronous client for simplicity in Gradio functions,
7
- # but Gradio supports async too. Let's use synchronous for now as it's often easier with Gradio tools.
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
- async def log_alert(
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
- # Validation logic is handled by Pydantic model implicitly if we use it,
32
- # but here we are constructing the payload.
33
- # Basic validation for severity
34
- if severity not in ["info", "warning", "critical"]:
35
- raise ValueError("Severity must be one of: info, warning, critical")
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- payload = {
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
- params = {}
68
- if provider_id is not None:
69
- params["provider_id"] = provider_id
70
- if severity is not None:
71
- params["severity"] = severity
72
-
73
- # Using POST /alerts/open with filters as per instructions, or GET if API supports params.
74
- # Instruction says: "GET or POST to /alerts/open with filters."
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
- async with httpx.AsyncClient(base_url=ALERT_API_BASE_URL) as client:
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
- payload = {"resolution_note": resolution_note}
102
-
103
- async with httpx.AsyncClient(base_url=ALERT_API_BASE_URL) as client:
104
- try:
105
- response = await client.post(f"/alerts/{alert_id}/resolve", json=payload)
106
- response.raise_for_status()
107
- return response.json()
108
- except httpx.HTTPStatusError as e:
109
- return {"error": f"HTTP error: {e.response.status_code}"}
110
- except Exception as e:
111
- return {"error": str(e)}
112
 
113
- async def summarize_alerts(window_days: Optional[int] = None) -> Dict[str, int]:
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
- payload = {}
121
- if window_days is not None:
122
- payload["window_days"] = window_days
123
-
124
- async with httpx.AsyncClient(base_url=ALERT_API_BASE_URL) as client:
125
- try:
126
- response = await client.post("/alerts/summary", json=payload)
127
- response.raise_for_status()
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()