Spaces:
Sleeping
Sleeping
google-labs-jules[bot]
commited on
Commit
·
54ea9d4
1
Parent(s):
da86ac7
Update README.md with CredentialWatch system context and fix tests
Browse files- Updated `README.md` to describe the full CredentialWatch architecture, including the 3 MCP servers, LangGraph agent, and Modal backend.
- Detailed the role of `cred_db_mcp_server` and its exposed tools.
- Added clear instructions for installation, configuration, and running with `uv`.
- Deleted obsolete `tests/test_cred_db_mcp.py` which referenced non-existent local models.
- Verified `tests/test_cred_db_mcp_server.py` passes and correctly mocks backend interactions.
- Added `pytest` as a dev dependency in `pyproject.toml`.
- README.md +110 -34
- pyproject.toml +5 -0
- tests/test_cred_db_mcp.py +0 -152
- uv.lock +8 -0
README.md
CHANGED
|
@@ -1,48 +1,124 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
|
| 3 |
|
| 4 |
-
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
1. **Onboard new providers**: Call `sync_provider_from_npi` to fetch details from the NPI registry and create a local record.
|
| 10 |
-
2. **Manage Credentials**: Use `add_or_update_credential` to keep license data up to date.
|
| 11 |
-
3. **Monitor Compliance**: Use `list_expiring_credentials` to proactively find providers who need to renew licenses.
|
| 12 |
-
4. **Context Retrieval**: Use `get_provider_snapshot` to get all known data about a provider before answering user questions.
|
| 13 |
|
| 14 |
-
##
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
```
|
| 23 |
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
| 27 |
-
**Endpoint:** `POST /mcp/tools/list_expiring_credentials`
|
| 28 |
-
**Headers:** `Content-Type: application/json`
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
{
|
| 33 |
-
"window_days": 90,
|
| 34 |
-
"dept": "Cardiology"
|
| 35 |
-
}
|
| 36 |
```
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CredentialWatch
|
| 2 |
|
| 3 |
+
**CredentialWatch** is a proactive healthcare credential management system built for the **Hugging Face MCP 1st Birthday / Gradio Agents Hackathon**. It leverages the **Model Context Protocol (MCP)**, **LangGraph** agents, **Gradio**, and **Modal** to unify fragmented credential data and alert on upcoming expiries.
|
| 4 |
|
| 5 |
+
## 🎯 Project Goal
|
| 6 |
|
| 7 |
+
CredentialWatch transforms messy, decentralized provider data (state licenses, board certifications, DEA numbers) into:
|
| 8 |
+
1. **A unified, queryable source of truth.**
|
| 9 |
+
2. **A proactive alerting system** for at-risk credentials.
|
| 10 |
|
| 11 |
+
The system is designed to solve the real-world problem of missed credential expiries leading to compliance issues, denied claims, and scheduling conflicts.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
## 🧩 Architecture
|
| 14 |
|
| 15 |
+
The system follows a microservice-like architecture with strict separation of concerns, orchestrated by a LangGraph agent.
|
| 16 |
+
|
| 17 |
+
### Components
|
| 18 |
+
|
| 19 |
+
1. **Agent UI (Gradio + LangGraph)**: The user interface where users interact with the agent (chat or sweep triggers).
|
| 20 |
+
2. **Three MCP Servers**:
|
| 21 |
+
* **`npi_mcp`**: Read-only access to the public NPPES NPI Registry.
|
| 22 |
+
* **`cred_db_mcp`**: (Hosted in this repo) Internal provider & credential database operations.
|
| 23 |
+
* **`alert_mcp`**: Alert logging and resolution management.
|
| 24 |
+
3. **Modal Backend**: Hosting FastAPI microservices and the SQLite database (`credentialwatch.db`).
|
| 25 |
+
|
| 26 |
+
### Data Flow
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
User <-> Gradio UI <-> LangGraph Agent
|
| 30 |
+
|
|
| 31 |
+
-----------------------------------
|
| 32 |
+
| | |
|
| 33 |
+
[npi_mcp] [cred_db_mcp] [alert_mcp]
|
| 34 |
+
| | |
|
| 35 |
+
(NPI_API) (CRED_API) (ALERT_API)
|
| 36 |
+
| | |
|
| 37 |
+
NPPES [SQLite DB] [SQLite DB]
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## 📂 Repository Contents: `cred_db_mcp_server`
|
| 41 |
+
|
| 42 |
+
This repository currently hosts the **Credential Database MCP Server** (`cred_db_mcp`). This component provides the tools necessary for the agent to read from and write to the internal provider/credential database.
|
| 43 |
+
|
| 44 |
+
### Exposed Tools
|
| 45 |
+
|
| 46 |
+
The server exposes the following MCP tools (via Gradio's MCP support):
|
| 47 |
+
|
| 48 |
+
* **`sync_provider_from_npi(npi)`**:
|
| 49 |
+
* Syncs a provider's data from the NPI registry (via backend) to the local database.
|
| 50 |
+
* *Usage*: Onboarding new providers.
|
| 51 |
+
* **`add_or_update_credential(provider_id, type, issuer, number, expiry_date)`**:
|
| 52 |
+
* Upserts a credential record for a provider.
|
| 53 |
+
* *Usage*: Keeping license data current.
|
| 54 |
+
* **`list_expiring_credentials(window_days, dept?, location?)`**:
|
| 55 |
+
* Returns a list of credentials expiring within the specified window (e.g., 90 days).
|
| 56 |
+
* *Usage*: Proactive monitoring and sweeps.
|
| 57 |
+
* **`get_provider_snapshot(provider_id?, npi?)`**:
|
| 58 |
+
* Returns provider info + all credentials + alerts.
|
| 59 |
+
* *Usage*: Context retrieval for user queries.
|
| 60 |
+
|
| 61 |
+
## 🚀 Getting Started
|
| 62 |
+
|
| 63 |
+
### Prerequisites
|
| 64 |
|
| 65 |
+
* **Python 3.11+**
|
| 66 |
+
* **uv** (recommended) or `pip`
|
| 67 |
+
* Access to the **Credential API Backend** (running locally or on Modal).
|
| 68 |
+
|
| 69 |
+
### Installation
|
| 70 |
+
|
| 71 |
+
1. Clone the repository:
|
| 72 |
+
```bash
|
| 73 |
+
git clone https://github.com/your-username/credential-watch-cred-db-mcp.git
|
| 74 |
+
cd credential-watch-cred-db-mcp
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
2. Install dependencies using `uv`:
|
| 78 |
+
```bash
|
| 79 |
+
uv sync
|
| 80 |
+
```
|
| 81 |
+
Or with `pip`:
|
| 82 |
+
```bash
|
| 83 |
+
pip install -e .
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### Configuration
|
| 87 |
+
|
| 88 |
+
The server requires the backend API URL to be configured. Create a `.env` file in the root directory:
|
| 89 |
+
|
| 90 |
+
```env
|
| 91 |
+
# URL of the backend Credential API service
|
| 92 |
+
CRED_API_BASE_URL=http://localhost:8000
|
| 93 |
```
|
| 94 |
|
| 95 |
+
### Running the Server
|
| 96 |
|
| 97 |
+
Start the Gradio MCP server:
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
```bash
|
| 100 |
+
uv run src/cred_db_mcp_server/main.py
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
```
|
| 102 |
|
| 103 |
+
The server will launch and:
|
| 104 |
+
1. Open a Gradio UI in your browser (usually `http://127.0.0.1:7860`) where you can manually test the tools.
|
| 105 |
+
2. Expose the MCP endpoints via SSE for the agent to connect to.
|
| 106 |
+
|
| 107 |
+
## 🧪 Testing
|
| 108 |
+
|
| 109 |
+
Run the unit tests using `pytest`:
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
uv run pytest
|
| 113 |
```
|
| 114 |
+
|
| 115 |
+
## 🛠 Tech Stack
|
| 116 |
+
|
| 117 |
+
* **Python 3.11**
|
| 118 |
+
* **Gradio 5+** (Frontend & MCP Server)
|
| 119 |
+
* **FastAPI / HTTPX** (Networking)
|
| 120 |
+
* **Pydantic** (Data validation)
|
| 121 |
+
* **uv** (Package management)
|
| 122 |
+
|
| 123 |
+
---
|
| 124 |
+
*Built for the Hugging Face MCP 1st Birthday Hackathon.*
|
pyproject.toml
CHANGED
|
@@ -29,3 +29,8 @@ packages = ["src/cred_db_mcp_server"]
|
|
| 29 |
[tool.pytest.ini_options]
|
| 30 |
pythonpath = "src"
|
| 31 |
testpaths = ["tests"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
[tool.pytest.ini_options]
|
| 30 |
pythonpath = "src"
|
| 31 |
testpaths = ["tests"]
|
| 32 |
+
|
| 33 |
+
[dependency-groups]
|
| 34 |
+
dev = [
|
| 35 |
+
"pytest>=9.0.1",
|
| 36 |
+
]
|
tests/test_cred_db_mcp.py
DELETED
|
@@ -1,152 +0,0 @@
|
|
| 1 |
-
import pytest
|
| 2 |
-
from fastapi.testclient import TestClient
|
| 3 |
-
from sqlalchemy import create_engine
|
| 4 |
-
from sqlalchemy.orm import sessionmaker
|
| 5 |
-
from sqlalchemy.pool import StaticPool
|
| 6 |
-
from datetime import date, timedelta
|
| 7 |
-
import os
|
| 8 |
-
|
| 9 |
-
from src.cred_db_mcp.main import app, get_db
|
| 10 |
-
from src.cred_db_mcp.db import Base
|
| 11 |
-
from src.cred_db_mcp.models import Provider, Credential
|
| 12 |
-
|
| 13 |
-
# Setup in-memory DB for tests
|
| 14 |
-
# Use StaticPool to share the in-memory DB across multiple sessions/connections
|
| 15 |
-
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
|
| 16 |
-
|
| 17 |
-
engine = create_engine(
|
| 18 |
-
SQLALCHEMY_DATABASE_URL,
|
| 19 |
-
connect_args={"check_same_thread": False},
|
| 20 |
-
poolclass=StaticPool
|
| 21 |
-
)
|
| 22 |
-
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 23 |
-
|
| 24 |
-
def override_get_db():
|
| 25 |
-
try:
|
| 26 |
-
db = TestingSessionLocal()
|
| 27 |
-
yield db
|
| 28 |
-
finally:
|
| 29 |
-
db.close()
|
| 30 |
-
|
| 31 |
-
app.dependency_overrides[get_db] = override_get_db
|
| 32 |
-
|
| 33 |
-
@pytest.fixture(scope="module")
|
| 34 |
-
def test_client():
|
| 35 |
-
# Create tables once for the module
|
| 36 |
-
Base.metadata.create_all(bind=engine)
|
| 37 |
-
client = TestClient(app)
|
| 38 |
-
yield client
|
| 39 |
-
Base.metadata.drop_all(bind=engine)
|
| 40 |
-
|
| 41 |
-
@pytest.fixture(autouse=True)
|
| 42 |
-
def clean_tables():
|
| 43 |
-
# Optional: Clear data between tests if needed, but for now just appending is fine
|
| 44 |
-
# as long as IDs/NPIs don't clash or we don't care about accumulation.
|
| 45 |
-
# To be safer, we can delete all data.
|
| 46 |
-
with engine.connect() as conn:
|
| 47 |
-
conn.execute(Credential.__table__.delete())
|
| 48 |
-
conn.execute(Provider.__table__.delete())
|
| 49 |
-
conn.commit()
|
| 50 |
-
|
| 51 |
-
@pytest.fixture
|
| 52 |
-
def db_session():
|
| 53 |
-
db = TestingSessionLocal()
|
| 54 |
-
yield db
|
| 55 |
-
db.close()
|
| 56 |
-
|
| 57 |
-
def test_read_root(test_client):
|
| 58 |
-
response = test_client.get("/")
|
| 59 |
-
assert response.status_code == 200
|
| 60 |
-
assert response.json()["service"] == "cred_db_mcp"
|
| 61 |
-
|
| 62 |
-
def test_add_and_snapshot_provider(test_client, db_session):
|
| 63 |
-
prov = Provider(
|
| 64 |
-
npi="999999", full_name="Test Doc", primary_specialty="General", is_active=True
|
| 65 |
-
)
|
| 66 |
-
db_session.add(prov)
|
| 67 |
-
db_session.commit()
|
| 68 |
-
|
| 69 |
-
response = test_client.post(
|
| 70 |
-
"/mcp/tools/get_provider_snapshot",
|
| 71 |
-
json={"npi": "999999"}
|
| 72 |
-
)
|
| 73 |
-
assert response.status_code == 200
|
| 74 |
-
data = response.json()
|
| 75 |
-
assert data["provider"]["full_name"] == "Test Doc"
|
| 76 |
-
assert len(data["credentials"]) == 0
|
| 77 |
-
|
| 78 |
-
def test_add_credential(test_client, db_session):
|
| 79 |
-
# Seed provider
|
| 80 |
-
prov = Provider(
|
| 81 |
-
npi="888888", full_name="Credential Doc", primary_specialty="Surgery", is_active=True
|
| 82 |
-
)
|
| 83 |
-
db_session.add(prov)
|
| 84 |
-
db_session.commit()
|
| 85 |
-
db_session.refresh(prov)
|
| 86 |
-
|
| 87 |
-
# Add Credential via tool
|
| 88 |
-
expiry = (date.today() + timedelta(days=100)).strftime("%Y-%m-%d")
|
| 89 |
-
response = test_client.post(
|
| 90 |
-
"/mcp/tools/add_or_update_credential",
|
| 91 |
-
json={
|
| 92 |
-
"provider_id": prov.id,
|
| 93 |
-
"type": "board_cert",
|
| 94 |
-
"issuer": "ABMS",
|
| 95 |
-
"number": "XYZ123",
|
| 96 |
-
"expiry_date": expiry
|
| 97 |
-
}
|
| 98 |
-
)
|
| 99 |
-
assert response.status_code == 200
|
| 100 |
-
data = response.json()
|
| 101 |
-
assert data["number"] == "XYZ123"
|
| 102 |
-
assert data["status"] == "active"
|
| 103 |
-
|
| 104 |
-
def test_list_expiring(test_client, db_session):
|
| 105 |
-
# Seed provider
|
| 106 |
-
prov = Provider(
|
| 107 |
-
npi="777777", full_name="Expiring Doc", dept="ER", location="NYC", is_active=True
|
| 108 |
-
)
|
| 109 |
-
db_session.add(prov)
|
| 110 |
-
db_session.commit()
|
| 111 |
-
db_session.refresh(prov)
|
| 112 |
-
|
| 113 |
-
# Add expiring credential (in 10 days)
|
| 114 |
-
# expiry date must be a date object for the model
|
| 115 |
-
expiry_1 = date.today() + timedelta(days=10)
|
| 116 |
-
cred = Credential(
|
| 117 |
-
provider_id=prov.id,
|
| 118 |
-
type="license",
|
| 119 |
-
issuer="State",
|
| 120 |
-
number="L1",
|
| 121 |
-
expiry_date=expiry_1,
|
| 122 |
-
status="active"
|
| 123 |
-
)
|
| 124 |
-
db_session.add(cred)
|
| 125 |
-
|
| 126 |
-
# Add non-expiring credential (in 100 days)
|
| 127 |
-
expiry_2 = date.today() + timedelta(days=100)
|
| 128 |
-
cred2 = Credential(
|
| 129 |
-
provider_id=prov.id,
|
| 130 |
-
type="license",
|
| 131 |
-
issuer="State",
|
| 132 |
-
number="L2",
|
| 133 |
-
expiry_date=expiry_2,
|
| 134 |
-
status="active"
|
| 135 |
-
)
|
| 136 |
-
db_session.add(cred2)
|
| 137 |
-
db_session.commit()
|
| 138 |
-
|
| 139 |
-
# Test tool
|
| 140 |
-
response = test_client.post(
|
| 141 |
-
"/mcp/tools/list_expiring_credentials",
|
| 142 |
-
json={
|
| 143 |
-
"window_days": 30,
|
| 144 |
-
"dept": "ER"
|
| 145 |
-
}
|
| 146 |
-
)
|
| 147 |
-
assert response.status_code == 200
|
| 148 |
-
data = response.json()
|
| 149 |
-
assert len(data) == 1
|
| 150 |
-
assert data[0]["credential"]["number"] == "L1"
|
| 151 |
-
assert data[0]["days_to_expiry"] == 10
|
| 152 |
-
assert data[0]["risk_score"] == 3 # < 30 days
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uv.lock
CHANGED
|
@@ -201,6 +201,11 @@ test = [
|
|
| 201 |
{ name = "pytest-asyncio" },
|
| 202 |
]
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
[package.metadata]
|
| 205 |
requires-dist = [
|
| 206 |
{ name = "fastapi", specifier = ">=0.100.0" },
|
|
@@ -215,6 +220,9 @@ requires-dist = [
|
|
| 215 |
]
|
| 216 |
provides-extras = ["test"]
|
| 217 |
|
|
|
|
|
|
|
|
|
|
| 218 |
[[package]]
|
| 219 |
name = "fastapi"
|
| 220 |
version = "0.123.0"
|
|
|
|
| 201 |
{ name = "pytest-asyncio" },
|
| 202 |
]
|
| 203 |
|
| 204 |
+
[package.dev-dependencies]
|
| 205 |
+
dev = [
|
| 206 |
+
{ name = "pytest" },
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
[package.metadata]
|
| 210 |
requires-dist = [
|
| 211 |
{ name = "fastapi", specifier = ">=0.100.0" },
|
|
|
|
| 220 |
]
|
| 221 |
provides-extras = ["test"]
|
| 222 |
|
| 223 |
+
[package.metadata.requires-dev]
|
| 224 |
+
dev = [{ name = "pytest", specifier = ">=9.0.1" }]
|
| 225 |
+
|
| 226 |
[[package]]
|
| 227 |
name = "fastapi"
|
| 228 |
version = "0.123.0"
|