Humanlearning commited on
Commit
cfd07be
·
0 Parent(s):

Clean commit for HF push

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .env.*
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ .DS_Store
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.13
README.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CredentialWatch Agent
2
+
3
+ MCP-powered agent system for monitoring healthcare provider credentials. This system uses LangGraph to orchestrate workflows for checking credential expiry and answering interactive queries, leveraging Model Context Protocol (MCP) to connect to external data sources.
4
+
5
+ ## Features
6
+
7
+ - **Interactive Query Agent**: Ask natural language questions about provider credentials.
8
+ - **Expiry Sweep Agent**: Automated workflow to check for expiring credentials and generate alerts.
9
+ - **MCP Integration**: Connects to NPI Registry, Credential Database, and Alerting systems via MCP.
10
+ - **Gradio UI**: User-friendly interface for interaction.
11
+
12
+ ## Prerequisites
13
+
14
+ - Python 3.11 or higher
15
+ - [uv](https://github.com/astral-sh/uv) (recommended)
16
+
17
+ ## Installation
18
+
19
+ 1. **Clone the repository:**
20
+ ```bash
21
+ git clone <repository_url>
22
+ cd credential_watch
23
+ ```
24
+
25
+ 2. **Install dependencies:**
26
+ ```bash
27
+ uv sync
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ Create a `.env` file in the root directory (or copy `.env.local`) and configure your environment variables:
33
+
34
+ ```env
35
+ OPENAI_API_KEY=your_openai_api_key
36
+ # MCP Server URLs (defaults shown)
37
+ NPI_MCP_URL=http://localhost:8001/sse
38
+ CRED_DB_MCP_URL=http://localhost:8002/sse
39
+ ALERT_MCP_URL=http://localhost:8003/sse
40
+ ```
41
+
42
+ ## Running the Application
43
+
44
+ To run the agent system with the Gradio UI using `uv`:
45
+
46
+ ```bash
47
+ uv run python src/credentialwatch_agent/main.py
48
+ ```
49
+ OR
50
+ ```bash
51
+ uv run -m credentialwatch_agent.main
52
+ ```
53
+
54
+ The UI will be available at `http://localhost:7860`.
55
+
56
+ ## Architecture
57
+
58
+ - **`src/credentialwatch_agent/agents/`**: Contains LangGraph workflow definitions.
59
+ - **`src/credentialwatch_agent/mcp_client.py`**: Handles connections to MCP servers.
60
+ - **`src/credentialwatch_agent/main.py`**: Entry point and Gradio UI.
61
+
62
+ ## MCP Servers
63
+
64
+ The agent expects the following MCP servers to be running:
65
+ 1. **NPI Server** (Port 8001)
66
+ 2. **Credential DB Server** (Port 8002)
67
+ 3. **Alert Server** (Port 8003)
68
+
69
+ If these servers are not reachable, the client will fall back to using mock data for demonstration purposes.
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ def main():
2
+ print("Hello from credential-watch!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
pyproject.toml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "credentialwatch_agent"
3
+ version = "0.1.0"
4
+ description = "MCP-powered agent system for monitoring healthcare provider credentials"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "langgraph>=0.0.10",
9
+ "langchain-openai>=0.0.5",
10
+ "pydantic>=2.0.0",
11
+ "mcp>=0.1.0",
12
+ "gradio>=4.0.0",
13
+ "python-dotenv>=1.0.0",
14
+ "httpx>=0.25.0"
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/credentialwatch_agent"]
src/credentialwatch_agent/__init__.py ADDED
File without changes
src/credentialwatch_agent/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (186 Bytes). View file
 
src/credentialwatch_agent/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (196 Bytes). View file
 
src/credentialwatch_agent/__pycache__/main.cpython-313.pyc ADDED
Binary file (4.9 kB). View file
 
src/credentialwatch_agent/__pycache__/mcp_client.cpython-310.pyc ADDED
Binary file (3.63 kB). View file
 
src/credentialwatch_agent/__pycache__/mcp_client.cpython-313.pyc ADDED
Binary file (6.3 kB). View file
 
src/credentialwatch_agent/agents/__pycache__/common.cpython-310.pyc ADDED
Binary file (1.13 kB). View file
 
src/credentialwatch_agent/agents/__pycache__/common.cpython-313.pyc ADDED
Binary file (1.36 kB). View file
 
src/credentialwatch_agent/agents/__pycache__/expiry_sweep.cpython-310.pyc ADDED
Binary file (2.54 kB). View file
 
src/credentialwatch_agent/agents/__pycache__/expiry_sweep.cpython-313.pyc ADDED
Binary file (4.07 kB). View file
 
src/credentialwatch_agent/agents/__pycache__/interactive_query.cpython-310.pyc ADDED
Binary file (3.05 kB). View file
 
src/credentialwatch_agent/agents/__pycache__/interactive_query.cpython-313.pyc ADDED
Binary file (4.24 kB). View file
 
src/credentialwatch_agent/agents/common.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict, List, Optional, Any, Dict, Annotated
2
+ import operator
3
+
4
+ class AgentState(TypedDict):
5
+ """
6
+ Common state for agents.
7
+ """
8
+ messages: List[Dict[str, Any]]
9
+ # Add other common fields if needed
10
+
11
+ class ExpirySweepState(TypedDict):
12
+ """
13
+ State for the expiry sweep graph.
14
+ """
15
+ providers: List[Dict[str, Any]]
16
+ alerts_created: int
17
+ errors: List[str]
18
+ summary: str
19
+ window_days: int
20
+
21
+ def merge_dicts(a: Dict, b: Dict) -> Dict:
22
+ return {**a, **b}
src/credentialwatch_agent/agents/expiry_sweep.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, List
2
+ from langgraph.graph import StateGraph, END
3
+ from credentialwatch_agent.agents.common import ExpirySweepState
4
+ from credentialwatch_agent.mcp_client import mcp_client
5
+
6
+ async def fetch_expiring_credentials(state: ExpirySweepState) -> Dict[str, Any]:
7
+ """
8
+ Fetches expiring credentials from the Credential DB MCP.
9
+ """
10
+ print("Fetching expiring credentials...")
11
+ # We check for a window defined in state or default to 90.
12
+ window_days = state.get("window_days", 90)
13
+ result = await mcp_client.call_tool(
14
+ "cred_db",
15
+ "list_expiring_credentials",
16
+ {"window_days": window_days}
17
+ )
18
+
19
+ # Handle mock/real response structure
20
+ expiring_items = result.get("expiring", []) if isinstance(result, dict) else []
21
+
22
+ return {"providers": expiring_items}
23
+
24
+ async def create_alerts(state: ExpirySweepState) -> Dict[str, Any]:
25
+ """
26
+ Creates alerts for the expiring credentials found.
27
+ """
28
+ expiring_items = state.get("providers", [])
29
+ alerts_count = 0
30
+ errors = []
31
+
32
+ print(f"Found {len(expiring_items)} expiring items. Creating alerts...")
33
+
34
+ for item in expiring_items:
35
+ try:
36
+ # Determine severity based on days_remaining
37
+ days = item.get("days_remaining", 90)
38
+ severity = "low"
39
+ if days <= 30:
40
+ severity = "critical"
41
+ elif days <= 60:
42
+ severity = "high"
43
+ elif days <= 90:
44
+ severity = "medium"
45
+
46
+ provider_id = item.get("provider_id")
47
+ credential_id = item.get("credential_id", "unknown") # Fallback if not provided in list
48
+ message = f"Credential {item.get('credential')} for {item.get('name')} expires in {days} days."
49
+
50
+ await mcp_client.call_tool(
51
+ "alert",
52
+ "log_alert",
53
+ {
54
+ "provider_id": provider_id,
55
+ "credential_id": credential_id,
56
+ "severity": severity,
57
+ "message": message
58
+ }
59
+ )
60
+ alerts_count += 1
61
+ except Exception as e:
62
+ errors.append(f"Failed to create alert for {item}: {e}")
63
+
64
+ return {"alerts_created": alerts_count, "errors": errors}
65
+
66
+ async def summarize_sweep(state: ExpirySweepState) -> Dict[str, Any]:
67
+ """
68
+ Summarizes the sweep results.
69
+ """
70
+ count = len(state.get("providers", []))
71
+ alerts = state.get("alerts_created", 0)
72
+ errors = state.get("errors", [])
73
+
74
+ summary = f"Sweep completed. Scanned {count} expiring items. Created {alerts} alerts."
75
+ if errors:
76
+ summary += f" Encountered {len(errors)} errors."
77
+
78
+ return {"summary": summary}
79
+
80
+ # Build the graph
81
+ workflow = StateGraph(ExpirySweepState)
82
+
83
+ workflow.add_node("fetch_expiring_credentials", fetch_expiring_credentials)
84
+ workflow.add_node("create_alerts", create_alerts)
85
+ workflow.add_node("summarize_sweep", summarize_sweep)
86
+
87
+ workflow.set_entry_point("fetch_expiring_credentials")
88
+
89
+ workflow.add_edge("fetch_expiring_credentials", "create_alerts")
90
+ workflow.add_edge("create_alerts", "summarize_sweep")
91
+ workflow.add_edge("summarize_sweep", END)
92
+
93
+ expiry_sweep_graph = workflow.compile()
src/credentialwatch_agent/agents/interactive_query.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Annotated, Literal, TypedDict, List
2
+ from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
3
+ from langchain_core.tools import tool
4
+ from langchain_openai import ChatOpenAI
5
+ from langgraph.graph import StateGraph, END
6
+ from langgraph.prebuilt import ToolNode
7
+ from credentialwatch_agent.mcp_client import mcp_client
8
+ from credentialwatch_agent.agents.common import AgentState
9
+
10
+ # --- Tool Definitions ---
11
+
12
+ @tool
13
+ async def search_providers(query: str, state: str = None, taxonomy: str = None):
14
+ """
15
+ Search for healthcare providers by name, state, or taxonomy.
16
+ Useful for finding a provider's NPI or internal ID.
17
+ """
18
+ return await mcp_client.call_tool("npi", "search_providers", {"query": query, "state": state, "taxonomy": taxonomy})
19
+
20
+ @tool
21
+ async def get_provider_by_npi(npi: str):
22
+ """
23
+ Get provider details using their NPI number.
24
+ """
25
+ return await mcp_client.call_tool("npi", "get_provider_by_npi", {"npi": npi})
26
+
27
+ @tool
28
+ async def list_expiring_credentials(window_days: int = 90):
29
+ """
30
+ List credentials expiring within the specified number of days.
31
+ """
32
+ return await mcp_client.call_tool("cred_db", "list_expiring_credentials", {"window_days": window_days})
33
+
34
+ @tool
35
+ async def get_provider_snapshot(provider_id: int = None, npi: str = None):
36
+ """
37
+ Get a comprehensive snapshot of a provider's credentials and status.
38
+ Provide either provider_id or npi.
39
+ """
40
+ return await mcp_client.call_tool("cred_db", "get_provider_snapshot", {"provider_id": provider_id, "npi": npi})
41
+
42
+ @tool
43
+ async def get_open_alerts():
44
+ """
45
+ Get a list of all currently open alerts.
46
+ """
47
+ return await mcp_client.call_tool("alert", "get_open_alerts", {})
48
+
49
+ tools = [
50
+ search_providers,
51
+ get_provider_by_npi,
52
+ list_expiring_credentials,
53
+ get_provider_snapshot,
54
+ get_open_alerts
55
+ ]
56
+
57
+ # --- Graph Definition ---
58
+
59
+ # We can use the prebuilt AgentState or our custom one.
60
+ # For simplicity, we'll use a state compatible with ToolNode (requires 'messages').
61
+
62
+ async def agent_node(state: AgentState):
63
+ """
64
+ Invokes the LLM to decide the next step.
65
+ """
66
+ messages = state["messages"]
67
+ model = ChatOpenAI(model="gpt-5.1", temperature=0) # Using gpt-5.1 as requested
68
+ # Note: User requested GPT-5.1. I should probably use the model name string they asked for if it's supported,
69
+ # or fallback to a standard one. I'll use "gpt-4o" as a safe high-quality default for now,
70
+ # or "gpt-5.1-preview" if I want to be cheeky, but let's stick to "gpt-4o" to ensure it works.
71
+ # Actually, the user said "LLM: OpenAI GPT-5.1". I should try to respect that string if possible,
72
+ # but I'll use "gpt-4o" and add a comment.
73
+
74
+ model_with_tools = model.bind_tools(tools)
75
+ response = await model_with_tools.ainvoke(messages)
76
+ return {"messages": [response]}
77
+
78
+ def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
79
+ """
80
+ Determines if the agent should continue to tools or end.
81
+ """
82
+ messages = state["messages"]
83
+ last_message = messages[-1]
84
+ if isinstance(last_message, AIMessage) and last_message.tool_calls:
85
+ return "tools"
86
+ return "__end__"
87
+
88
+ workflow = StateGraph(AgentState)
89
+
90
+ workflow.add_node("agent", agent_node)
91
+ workflow.add_node("tools", ToolNode(tools))
92
+
93
+ workflow.set_entry_point("agent")
94
+
95
+ workflow.add_conditional_edges(
96
+ "agent",
97
+ should_continue,
98
+ )
99
+ workflow.add_edge("tools", "agent")
100
+
101
+ interactive_query_graph = workflow.compile()
src/credentialwatch_agent/main.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ from typing import Dict, Any, List
4
+ import gradio as gr
5
+ from dotenv import load_dotenv
6
+ from langchain_core.messages import HumanMessage, AIMessage
7
+
8
+ from credentialwatch_agent.mcp_client import mcp_client
9
+ from credentialwatch_agent.agents.expiry_sweep import expiry_sweep_graph
10
+ from credentialwatch_agent.agents.interactive_query import interactive_query_graph
11
+
12
+ # Load environment variables
13
+ load_dotenv()
14
+
15
+ async def run_expiry_sweep(window_days: int = 90) -> Dict[str, Any]:
16
+ """
17
+ Runs the expiry sweep workflow.
18
+ """
19
+ print(f"Starting expiry sweep for {window_days} days...")
20
+ # Initialize state
21
+ initial_state = {
22
+ "providers": [],
23
+ "alerts_created": 0,
24
+ "errors": [],
25
+ "summary": "",
26
+ "window_days": window_days
27
+ }
28
+
29
+ # Run the graph
30
+ # Note: The graph expects to fetch data itself, so initial state can be minimal.
31
+ # We might want to pass window_days if the graph supported dynamic config in state,
32
+ # but for now the graph hardcodes 90 or uses tool defaults.
33
+ # To make it dynamic, we'd need to update the graph to read from state.
34
+ # For this hackathon, we'll assume the graph handles it or we pass it via a modified state if needed.
35
+ # The current implementation of fetch_expiring_credentials uses a hardcoded 90 or tool default.
36
+
37
+ final_state = await expiry_sweep_graph.ainvoke(initial_state)
38
+ return {
39
+ "summary": final_state.get("summary"),
40
+ "alerts_created": final_state.get("alerts_created"),
41
+ "errors": final_state.get("errors")
42
+ }
43
+
44
+ async def run_chat_turn(message: str, history: List[List[str]]) -> str:
45
+ """
46
+ Runs a turn of the interactive query agent.
47
+ """
48
+ # Convert history to LangChain format
49
+ messages = []
50
+ for human, ai in history:
51
+ messages.append(HumanMessage(content=human))
52
+ messages.append(AIMessage(content=ai))
53
+ messages.append(HumanMessage(content=message))
54
+
55
+ initial_state = {"messages": messages}
56
+
57
+ # Run the graph
58
+ final_state = await interactive_query_graph.ainvoke(initial_state)
59
+
60
+ # Extract the last message
61
+ last_message = final_state["messages"][-1]
62
+ return last_message.content
63
+
64
+ # --- Gradio UI ---
65
+
66
+ async def start_app():
67
+ """Initializes the app and connects to MCP servers."""
68
+ print("Connecting to MCP servers...")
69
+ await mcp_client.connect()
70
+
71
+ async def stop_app():
72
+ """Closes connections."""
73
+ print("Closing MCP connections...")
74
+ await mcp_client.close()
75
+
76
+ with gr.Blocks(title="CredentialWatch") as demo:
77
+ gr.Markdown("# CredentialWatch Agent System")
78
+
79
+ with gr.Tab("Interactive Query"):
80
+ gr.Markdown("Ask questions about provider credentials, e.g., 'Who has expiring licenses?'")
81
+ chat_interface = gr.ChatInterface(fn=run_chat_turn)
82
+
83
+ with gr.Tab("Expiry Sweep"):
84
+ gr.Markdown("Run a batch sweep to check for expiring credentials and create alerts.")
85
+ with gr.Row():
86
+ sweep_btn = gr.Button("Run Sweep", variant="primary")
87
+
88
+ sweep_output = gr.JSON(label="Sweep Results")
89
+
90
+ sweep_btn.click(fn=run_expiry_sweep, inputs=[], outputs=[sweep_output])
91
+
92
+ # Startup/Shutdown hooks
93
+ # Gradio doesn't have native async startup hooks easily exposed in Blocks without mounting to FastAPI.
94
+ # But we can run the connect logic when the script starts if we run it via `uv run`.
95
+ # For a proper app, we'd use lifespan events in FastAPI.
96
+ # Here, we will just connect globally on import or first use if possible,
97
+ # or use a startup event if we were using `gr.mount_gradio_app`.
98
+ # For simplicity in this script, we'll rely on the global mcp_client.connect() being called
99
+ # or we can wrap the demo launch.
100
+
101
+ if __name__ == "__main__":
102
+ # Simple async wrapper to run the app
103
+ loop = asyncio.new_event_loop()
104
+ asyncio.set_event_loop(loop)
105
+
106
+ try:
107
+ loop.run_until_complete(mcp_client.connect())
108
+ demo.launch(server_name="0.0.0.0", server_port=7860)
109
+ except KeyboardInterrupt:
110
+ pass
111
+ finally:
112
+ loop.run_until_complete(mcp_client.close())
src/credentialwatch_agent/mcp_client.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from typing import Any, Dict, List, Optional
4
+ from contextlib import AsyncExitStack
5
+
6
+ from mcp import ClientSession, StdioServerParameters
7
+ from mcp.client.sse import sse_client
8
+ from mcp.client.stdio import stdio_client
9
+
10
+ class MCPClient:
11
+ """
12
+ Abstraction for calling MCP tools from multiple servers.
13
+ Manages connections to NPI, Credential DB, and Alert MCP servers.
14
+ """
15
+
16
+ def __init__(self):
17
+ self.npi_url = os.getenv("NPI_MCP_URL", "http://localhost:8001/sse")
18
+ self.cred_db_url = os.getenv("CRED_DB_MCP_URL", "http://localhost:8002/sse")
19
+ self.alert_url = os.getenv("ALERT_MCP_URL", "http://localhost:8003/sse")
20
+
21
+ self._exit_stack = AsyncExitStack()
22
+ self._sessions: Dict[str, ClientSession] = {}
23
+
24
+ async def connect(self):
25
+ """Establishes connections to all MCP servers."""
26
+ # In a real app, we might want to connect lazily or in parallel.
27
+ # For simplicity, we'll try to connect to all.
28
+
29
+ # Connect to NPI MCP
30
+ try:
31
+ # Note: mcp.client.sse.sse_client is a context manager that yields (read_stream, write_stream)
32
+ # We need to keep the context open.
33
+ npi_transport = await self._exit_stack.enter_async_context(sse_client(self.npi_url))
34
+ self._sessions["npi"] = await self._exit_stack.enter_async_context(
35
+ ClientSession(npi_transport[0], npi_transport[1])
36
+ )
37
+ await self._sessions["npi"].initialize()
38
+ except Exception as e:
39
+ print(f"Failed to connect to NPI MCP at {self.npi_url}: {e}")
40
+
41
+ # Connect to Cred DB MCP
42
+ try:
43
+ cred_transport = await self._exit_stack.enter_async_context(sse_client(self.cred_db_url))
44
+ self._sessions["cred_db"] = await self._exit_stack.enter_async_context(
45
+ ClientSession(cred_transport[0], cred_transport[1])
46
+ )
47
+ await self._sessions["cred_db"].initialize()
48
+ except Exception as e:
49
+ print(f"Failed to connect to Cred DB MCP at {self.cred_db_url}: {e}")
50
+
51
+ # Connect to Alert MCP
52
+ try:
53
+ alert_transport = await self._exit_stack.enter_async_context(sse_client(self.alert_url))
54
+ self._sessions["alert"] = await self._exit_stack.enter_async_context(
55
+ ClientSession(alert_transport[0], alert_transport[1])
56
+ )
57
+ await self._sessions["alert"].initialize()
58
+ except Exception as e:
59
+ print(f"Failed to connect to Alert MCP at {self.alert_url}: {e}")
60
+
61
+ async def close(self):
62
+ """Closes all connections."""
63
+ await self._exit_stack.aclose()
64
+
65
+ async def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> Any:
66
+ """Calls a tool on a specific MCP server."""
67
+ session = self._sessions.get(server_name)
68
+ if not session:
69
+ # Fallback for testing/mocking if connection failed
70
+ print(f"Warning: No active session for {server_name}. Returning mock data.")
71
+ return self._get_mock_response(server_name, tool_name, arguments)
72
+
73
+ result = await session.call_tool(tool_name, arguments)
74
+ return result
75
+
76
+ def _get_mock_response(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> Any:
77
+ """Returns mock data when MCP server is unavailable."""
78
+ if server_name == "npi":
79
+ if tool_name == "search_providers":
80
+ return {"providers": [{"npi": "1234567890", "name": "Dr. Jane Doe", "taxonomy": "Cardiology"}]}
81
+ if tool_name == "get_provider_by_npi":
82
+ return {"npi": arguments.get("npi"), "name": "Dr. Jane Doe", "licenses": []}
83
+
84
+ if server_name == "cred_db":
85
+ if tool_name == "list_expiring_credentials":
86
+ return {"expiring": [{"provider_id": 1, "name": "Dr. Jane Doe", "credential": "Medical License", "days_remaining": 25}]}
87
+ if tool_name == "get_provider_snapshot":
88
+ return {"name": "Dr. Jane Doe", "status": "Active", "credentials": []}
89
+
90
+ if server_name == "alert":
91
+ if tool_name == "log_alert":
92
+ return {"success": True, "alert_id": 101}
93
+ if tool_name == "get_open_alerts":
94
+ return {"alerts": []}
95
+
96
+ return {"error": "Mock data not found for this tool"}
97
+
98
+ # Global instance
99
+ mcp_client = MCPClient()
uv.lock ADDED
The diff for this file is too large to render. See raw diff