visproj commited on
Commit
0d10048
·
verified ·
1 Parent(s): f1fa413

initial commit

Browse files
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy application code
10
+ COPY . .
11
+
12
+ # Environment variables
13
+ ENV ENABLED_PROVIDERS="openfda,clinical_guidelines,cms_pricing"
14
+ ENV LOG_LEVEL="INFO"
15
+ ENV PYTHONUNBUFFERED=1
16
+
17
+ # Expose port
18
+ EXPOSE 7860
19
+
20
+ # Health check
21
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
22
+ CMD python -c "import httpx; httpx.get('http://localhost:7860/health')"
23
+
24
+ # Run HTTP streaming server
25
+ CMD ["python", "server_http.py"]
README.md CHANGED
@@ -1,12 +1,157 @@
1
- ---
2
- title: Healthcare Api Mcp
3
- emoji: 😻
4
- colorFrom: red
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- license: other
9
- short_description: multi healthcare api mcp wrapper
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Healthcare API MCP
2
+
3
+ A Model Context Protocol (MCP) server providing access to public healthcare data APIs including FDA adverse events, clinical guidelines, and Medicare pricing information.
4
+
5
+ ## Features
6
+
7
+ ### OpenFDA Provider (14 tools)
8
+ - **Drug Adverse Events**: Query FAERS database for medication safety information
9
+ - **Drug Labels**: Search FDA-approved drug labeling information
10
+ - **Drug Recalls**: Find drug recall and enforcement reports
11
+ - **NDC Directory**: Look up National Drug Codes
12
+ - **Drugs@FDA**: Search approved drug products
13
+ - **Device Events & Recalls**: Medical device adverse events and recalls
14
+ - **Device Classifications**: Device classification database
15
+ - **510(k) & PMA**: Device clearances and approvals
16
+ - **Food Recalls & Events**: Food safety information
17
+
18
+ ### Clinical Guidelines Provider (5 tools)
19
+ - **Preventive Recommendations**: USPSTF evidence-based screening guidelines
20
+ - **Vaccine Schedule**: CDC immunization recommendations
21
+ - **Search Guidelines**: Find guidelines by condition or screening type
22
+ - **Lifestyle Recommendations**: Evidence-based wellness guidance
23
+ - **Care Plan Generator**: Comprehensive 6-month care planning
24
+
25
+ ### CMS Pricing Provider (5 tools)
26
+ - **Medicare Fee Schedule**: Get reimbursement rates by HCPCS code
27
+ - **Procedure Cost Estimates**: Typical cost ranges by procedure
28
+ - **Search Procedure Codes**: Find HCPCS/CPT codes
29
+ - **Facility Cost Comparison**: Compare prices across facilities
30
+ - **Out-of-Pocket Estimates**: Calculate patient responsibility
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install -r requirements.txt
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ Set environment variables to configure the server:
41
+
42
+ ```bash
43
+ # Enable specific providers (comma-separated)
44
+ export ENABLED_PROVIDERS="openfda,clinical_guidelines,cms_pricing"
45
+
46
+ # Optional: OpenFDA API key for higher rate limits
47
+ export OPENFDA_API_KEY="your_api_key_here"
48
+
49
+ # Logging level
50
+ export LOG_LEVEL="INFO"
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ### Run as MCP Server (stdio)
56
+
57
+ ```bash
58
+ python -m healthcare_api_mcp
59
+ ```
60
+
61
+ ### Use with Claude Desktop
62
+
63
+ Add to your Claude Desktop config (`claude_desktop_config.json`):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "healthcare-api": {
69
+ "command": "python",
70
+ "args": ["-m", "healthcare_api_mcp"],
71
+ "env": {
72
+ "ENABLED_PROVIDERS": "openfda,clinical_guidelines,cms_pricing"
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Filter Providers
80
+
81
+ Run only specific providers:
82
+
83
+ ```bash
84
+ # Only FDA data
85
+ ENABLED_PROVIDERS=openfda python -m healthcare_api_mcp
86
+
87
+ # Only clinical guidelines
88
+ ENABLED_PROVIDERS=clinical_guidelines python -m healthcare_api_mcp
89
+
90
+ # FDA and CMS pricing
91
+ ENABLED_PROVIDERS=openfda,cms_pricing python -m healthcare_api_mcp
92
+ ```
93
+
94
+ ## Tool Naming
95
+
96
+ All tools are namespaced by provider:
97
+ - OpenFDA: `openfda_*` (e.g., `openfda_search_adverse_events`)
98
+ - Clinical Guidelines: `clinical_*` (e.g., `clinical_get_vaccine_schedule`)
99
+ - CMS Pricing: `cms_*` (e.g., `cms_get_medicare_fee`)
100
+
101
+ ## Example Queries
102
+
103
+ ### Check Drug Safety
104
+ ```
105
+ Agent: "Check if Simvastatin has any FDA recalls"
106
+ → Uses: openfda_search_drug_recalls
107
+ ```
108
+
109
+ ### Get Preventive Care Recommendations
110
+ ```
111
+ Agent: "What screenings does a 55-year-old female need?"
112
+ → Uses: clinical_get_preventive_recommendations
113
+ ```
114
+
115
+ ### Estimate Procedure Cost
116
+ ```
117
+ Agent: "How much does a colonoscopy cost with Medicare?"
118
+ → Uses: cms_estimate_procedure_cost, cms_estimate_out_of_pocket
119
+ ```
120
+
121
+ ## Data Sources
122
+
123
+ - **OpenFDA**: Real-time access to FDA's public APIs
124
+ - **Clinical Guidelines**: Evidence-based recommendations from USPSTF, CDC, AHRQ
125
+ - **CMS Pricing**: Sample data based on Medicare fee schedules (2024)
126
+
127
+ ## Architecture
128
+
129
+ ```
130
+ healthcare_api_mcp/
131
+ ├── server.py # Main MCP server with stdio
132
+ ├── config.py # Configuration management
133
+ ├── core/
134
+ │ ├── base_provider.py # Abstract provider base class
135
+ │ ├── decorators.py # @safe_json_return, @with_retry
136
+ │ └── http_client.py # Shared HTTP client factory
137
+ └── providers/
138
+ ├── openfda_provider.py # FDA APIs
139
+ ├── clinical_guidelines_provider.py
140
+ └── cms_pricing_provider.py
141
+ ```
142
+
143
+ ## Benefits
144
+
145
+ ✅ **Zero duplication**: Shared utilities across all providers
146
+ ✅ **Proper JSON returns**: All tools return JSON strings (no dict inconsistencies)
147
+ ✅ **Modular**: Enable only the providers you need
148
+ ✅ **Agent-friendly**: Standard MCP stdio protocol
149
+ ✅ **Production-ready**: Error handling, retry logic, logging
150
+
151
+ ## License
152
+
153
+ Copyright (c) 2025. All Rights Reserved.
154
+
155
+ ## HuggingFace MCP 1st Birthday Hackathon
156
+
157
+ This project is submitted for Track 1 of the HuggingFace MCP 1st Birthday Hackathon.
__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """Healthcare API MCP - Public health data APIs."""
2
+
3
+ __version__ = "1.0.0"
__main__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """Entry point for running the MCP server as a module."""
2
+
3
+ from .server import main
4
+ import asyncio
5
+
6
+ if __name__ == "__main__":
7
+ asyncio.run(main())
config.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration for Healthcare API MCP."""
2
+
3
+ import os
4
+ from typing import List, Optional
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ProviderConfig(BaseModel):
9
+ """Configuration for a provider."""
10
+ name: str
11
+ enabled: bool = True
12
+ api_key: Optional[str] = None
13
+
14
+
15
+ class MCPConfig(BaseModel):
16
+ """Main MCP server configuration."""
17
+
18
+ # Enabled providers (comma-separated or list)
19
+ enabled_providers: List[str] = Field(
20
+ default_factory=lambda: ["openfda", "clinical_guidelines", "cms_pricing"]
21
+ )
22
+
23
+ # OpenFDA API key (optional but recommended for higher rate limits)
24
+ openfda_api_key: Optional[str] = Field(default=None)
25
+
26
+ # Logging level
27
+ log_level: str = Field(default="INFO")
28
+
29
+ @classmethod
30
+ def from_env(cls) -> "MCPConfig":
31
+ """Load configuration from environment variables."""
32
+
33
+ # Parse enabled providers
34
+ enabled_str = os.getenv("ENABLED_PROVIDERS", "")
35
+ if enabled_str:
36
+ enabled_providers = [p.strip() for p in enabled_str.split(",")]
37
+ else:
38
+ enabled_providers = ["openfda", "clinical_guidelines", "cms_pricing"]
39
+
40
+ return cls(
41
+ enabled_providers=enabled_providers,
42
+ openfda_api_key=os.getenv("OPENFDA_API_KEY"),
43
+ log_level=os.getenv("LOG_LEVEL", "INFO")
44
+ )
core/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """Core utilities for Healthcare API MCP."""
2
+
3
+ from .base_provider import BaseProvider
4
+ from .decorators import safe_json_return, with_retry
5
+ from .http_client import create_http_client
6
+
7
+ __all__ = ["BaseProvider", "safe_json_return", "with_retry", "create_http_client"]
core/base_provider.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base provider class for healthcare APIs."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List, Callable
5
+ import httpx
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class BaseProvider(ABC):
12
+ """Abstract base class for healthcare API providers."""
13
+
14
+ def __init__(self, name: str, client: httpx.AsyncClient):
15
+ """
16
+ Initialize provider.
17
+
18
+ Args:
19
+ name: Provider name (used for tool prefixing)
20
+ client: Async HTTP client
21
+ """
22
+ self.name = name
23
+ self.client = client
24
+ self._tools = []
25
+
26
+ @abstractmethod
27
+ async def initialize(self) -> None:
28
+ """
29
+ Initialize provider (e.g., test API connection).
30
+ Override in subclasses if needed.
31
+ """
32
+ pass
33
+
34
+ @abstractmethod
35
+ def get_tools(self) -> List[Callable]:
36
+ """
37
+ Return list of MCP tool functions provided by this provider.
38
+
39
+ Returns:
40
+ List of tool functions
41
+ """
42
+ pass
43
+
44
+ async def cleanup(self) -> None:
45
+ """
46
+ Cleanup provider resources.
47
+ Override in subclasses if needed.
48
+ """
49
+ pass
50
+
51
+ def register_tool(self, func: Callable) -> None:
52
+ """Register a tool function."""
53
+ self._tools.append(func)
54
+
55
+ @property
56
+ def tools(self) -> List[Callable]:
57
+ """Get all registered tools."""
58
+ return self._tools
core/decorators.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared decorators for MCP tools."""
2
+
3
+ import functools
4
+ import json
5
+ import logging
6
+ from typing import Any, Callable, TypeVar
7
+ from tenacity import retry, stop_after_attempt, wait_exponential
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ F = TypeVar('F', bound=Callable[..., Any])
12
+
13
+
14
+ def safe_json_return(func: F) -> F:
15
+ """
16
+ Decorator that ensures MCP tools always return JSON strings.
17
+ Handles dicts, lists, None, and exceptions consistently.
18
+ """
19
+ @functools.wraps(func)
20
+ async def async_wrapper(*args, **kwargs):
21
+ try:
22
+ result = await func(*args, **kwargs)
23
+
24
+ # Handle None
25
+ if result is None:
26
+ return json.dumps({"result": None})
27
+
28
+ # Handle dict or list
29
+ if isinstance(result, (dict, list)):
30
+ return json.dumps(result, ensure_ascii=False)
31
+
32
+ # Handle string (might already be JSON)
33
+ if isinstance(result, str):
34
+ return result
35
+
36
+ # Handle other types
37
+ return json.dumps({"result": str(result)})
38
+
39
+ except Exception as e:
40
+ logger.exception(f"Error in {func.__name__}")
41
+ return json.dumps({
42
+ "error": str(e),
43
+ "error_type": type(e).__name__
44
+ })
45
+
46
+ @functools.wraps(func)
47
+ def sync_wrapper(*args, **kwargs):
48
+ try:
49
+ result = func(*args, **kwargs)
50
+
51
+ if result is None:
52
+ return json.dumps({"result": None})
53
+
54
+ if isinstance(result, (dict, list)):
55
+ return json.dumps(result, ensure_ascii=False)
56
+
57
+ if isinstance(result, str):
58
+ return result
59
+
60
+ return json.dumps({"result": str(result)})
61
+
62
+ except Exception as e:
63
+ logger.exception(f"Error in {func.__name__}")
64
+ return json.dumps({
65
+ "error": str(e),
66
+ "error_type": type(e).__name__
67
+ })
68
+
69
+ # Return appropriate wrapper based on function type
70
+ import inspect
71
+ if inspect.iscoroutinefunction(func):
72
+ return async_wrapper
73
+ else:
74
+ return sync_wrapper
75
+
76
+
77
+ def with_retry(func: F) -> F:
78
+ """
79
+ Decorator for retrying failed HTTP requests with exponential backoff.
80
+ """
81
+ return retry(
82
+ stop=stop_after_attempt(3),
83
+ wait=wait_exponential(multiplier=1, min=2, max=8),
84
+ reraise=True
85
+ )(func)
core/http_client.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared HTTP client factory."""
2
+
3
+ import httpx
4
+ from typing import Optional, Dict
5
+
6
+
7
+ def create_http_client(
8
+ timeout: float = 30.0,
9
+ headers: Optional[Dict[str, str]] = None,
10
+ follow_redirects: bool = True
11
+ ) -> httpx.AsyncClient:
12
+ """
13
+ Create a configured async HTTP client.
14
+
15
+ Args:
16
+ timeout: Request timeout in seconds
17
+ headers: Default headers to include
18
+ follow_redirects: Whether to follow redirects
19
+
20
+ Returns:
21
+ Configured httpx.AsyncClient
22
+ """
23
+ default_headers = {
24
+ "Accept": "application/json",
25
+ "Content-Type": "application/json",
26
+ }
27
+
28
+ if headers:
29
+ default_headers.update(headers)
30
+
31
+ return httpx.AsyncClient(
32
+ timeout=timeout,
33
+ headers=default_headers,
34
+ follow_redirects=follow_redirects
35
+ )
providers/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Healthcare API providers."""
2
+
3
+ from typing import Dict, Type
4
+ from ..core.base_provider import BaseProvider
5
+
6
+ # Provider registry
7
+ PROVIDER_REGISTRY: Dict[str, Type[BaseProvider]] = {}
8
+
9
+
10
+ def register_provider(name: str):
11
+ """Decorator to register a provider."""
12
+ def decorator(cls: Type[BaseProvider]):
13
+ PROVIDER_REGISTRY[name] = cls
14
+ return cls
15
+ return decorator
16
+
17
+
18
+ def get_provider_class(name: str) -> Type[BaseProvider]:
19
+ """Get provider class by name."""
20
+ return PROVIDER_REGISTRY.get(name)
providers/clinical_guidelines_provider.py ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Clinical Guidelines Provider for USPSTF, CDC, and AHRQ guidelines."""
2
+
3
+ import logging
4
+ from typing import List, Dict, Any, Callable
5
+ import httpx
6
+
7
+ from ..core.base_provider import BaseProvider
8
+ from ..core.decorators import safe_json_return
9
+ from . import register_provider
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Evidence-based preventive care guidelines
14
+ PREVENTIVE_GUIDELINES = {
15
+ "diabetes_screening": [
16
+ {
17
+ "title": "Type 2 Diabetes Screening",
18
+ "recommendation": "Screen for prediabetes and type 2 diabetes in adults aged 35 to 70 years who have overweight or obesity",
19
+ "grade": "B",
20
+ "age_range": "35-70 years",
21
+ "frequency": "Every 3 years if normal",
22
+ "source": "USPSTF",
23
+ "conditions": ["overweight", "obesity"],
24
+ "citation": "USPSTF 2021"
25
+ }
26
+ ],
27
+ "colorectal_screening": [
28
+ {
29
+ "title": "Colorectal Cancer Screening",
30
+ "recommendation": "Screen for colorectal cancer starting at age 45 years",
31
+ "grade": "A",
32
+ "age_range": "45-75 years",
33
+ "frequency": "Every 10 years (colonoscopy) or annually (FIT)",
34
+ "source": "USPSTF",
35
+ "methods": ["Colonoscopy every 10 years", "FIT annually", "CT colonography every 5 years"],
36
+ "citation": "USPSTF 2021"
37
+ }
38
+ ],
39
+ "hypertension_screening": [
40
+ {
41
+ "title": "Hypertension Screening",
42
+ "recommendation": "Screen for high blood pressure in adults 18 years or older",
43
+ "grade": "A",
44
+ "age_range": "18+ years",
45
+ "frequency": "Annually for adults 40+ or at increased risk; every 3-5 years for adults 18-39 with normal BP",
46
+ "source": "USPSTF",
47
+ "citation": "USPSTF 2021"
48
+ }
49
+ ],
50
+ "cholesterol_screening": [
51
+ {
52
+ "title": "Statin Use for Primary Prevention of CVD",
53
+ "recommendation": "Screen for cardiovascular disease risk factors beginning at age 40",
54
+ "grade": "B",
55
+ "age_range": "40-75 years",
56
+ "frequency": "Every 4-6 years",
57
+ "source": "USPSTF",
58
+ "conditions": ["1+ CVD risk factors"],
59
+ "citation": "USPSTF 2016"
60
+ }
61
+ ]
62
+ }
63
+
64
+ # CDC vaccine schedule recommendations
65
+ VACCINE_SCHEDULE = {
66
+ "influenza": {
67
+ "vaccine": "Influenza (Flu)",
68
+ "recommendation": "Annual vaccination for all persons aged 6 months and older",
69
+ "age_groups": ["6 months+"],
70
+ "frequency": "Annually",
71
+ "source": "CDC",
72
+ "priority_groups": ["65+", "Pregnant", "Chronic conditions"]
73
+ },
74
+ "covid19": {
75
+ "vaccine": "COVID-19",
76
+ "recommendation": "Vaccination recommended for all persons 6 months and older",
77
+ "age_groups": ["6 months+"],
78
+ "frequency": "Updated annually",
79
+ "source": "CDC",
80
+ "priority_groups": ["65+", "Immunocompromised", "Healthcare workers"]
81
+ },
82
+ "pneumococcal": {
83
+ "vaccine": "Pneumococcal (PCV/PPSV)",
84
+ "recommendation": "Vaccination for adults 65 years or older and younger adults with certain conditions",
85
+ "age_groups": ["65+", "19-64 with risk factors"],
86
+ "frequency": "One-time or based on risk",
87
+ "source": "CDC",
88
+ "conditions": ["Chronic heart/lung disease", "Diabetes", "Immunocompromised"]
89
+ },
90
+ "shingles": {
91
+ "vaccine": "Shingles (Zoster)",
92
+ "recommendation": "2-dose series for adults 50 years and older",
93
+ "age_groups": ["50+"],
94
+ "frequency": "2 doses, 2-6 months apart",
95
+ "source": "CDC"
96
+ },
97
+ "tdap": {
98
+ "vaccine": "Tdap (Tetanus, Diphtheria, Pertussis)",
99
+ "recommendation": "One dose of Tdap, then Td booster every 10 years",
100
+ "age_groups": ["All adults"],
101
+ "frequency": "Every 10 years",
102
+ "source": "CDC"
103
+ },
104
+ "hepatitis_b": {
105
+ "vaccine": "Hepatitis B",
106
+ "recommendation": "Vaccination for adults at increased risk",
107
+ "age_groups": ["19-59 years (universal)", "60+ with risk factors"],
108
+ "frequency": "2 or 3-dose series",
109
+ "source": "CDC",
110
+ "risk_factors": ["Healthcare workers", "Diabetes", "Chronic liver disease"]
111
+ }
112
+ }
113
+
114
+
115
+ @register_provider("clinical_guidelines")
116
+ class ClinicalGuidelinesProvider(BaseProvider):
117
+ """Provider for clinical guidelines and preventive care recommendations."""
118
+
119
+ def __init__(self, client: httpx.AsyncClient):
120
+ super().__init__("clinical_guidelines", client)
121
+
122
+ async def initialize(self) -> None:
123
+ """Initialize clinical guidelines provider."""
124
+ logger.info("Clinical Guidelines provider initialized")
125
+
126
+ def get_tools(self) -> List[Callable]:
127
+ """Return all clinical guideline tools."""
128
+ return [
129
+ self.clinical_get_preventive_recommendations,
130
+ self.clinical_get_vaccine_schedule,
131
+ self.clinical_search_guidelines,
132
+ self.clinical_get_lifestyle_recommendations,
133
+ self.clinical_generate_care_plan,
134
+ ]
135
+
136
+ @safe_json_return
137
+ async def clinical_get_preventive_recommendations(
138
+ self,
139
+ age: int,
140
+ sex: str,
141
+ conditions: List[str] = None
142
+ ) -> Dict[str, Any]:
143
+ """
144
+ Get personalized preventive care recommendations based on USPSTF guidelines.
145
+
146
+ Args:
147
+ age: Patient age in years
148
+ sex: Patient sex (male/female)
149
+ conditions: List of existing conditions (e.g., ["diabetes", "overweight"])
150
+
151
+ Returns:
152
+ Evidence-based screening and prevention recommendations with grades
153
+ """
154
+ if conditions is None:
155
+ conditions = []
156
+
157
+ recommendations = []
158
+ condition_lower = [c.lower() for c in conditions]
159
+
160
+ # Diabetes screening
161
+ if 35 <= age <= 70 and any(c in condition_lower for c in ["overweight", "obesity", "prediabetes"]):
162
+ recommendations.append({
163
+ **PREVENTIVE_GUIDELINES["diabetes_screening"][0],
164
+ "applicable": True,
165
+ "rationale": "Age and BMI criteria met"
166
+ })
167
+
168
+ # Colorectal cancer screening
169
+ if 45 <= age <= 75:
170
+ recommendations.append({
171
+ **PREVENTIVE_GUIDELINES["colorectal_screening"][0],
172
+ "applicable": True,
173
+ "rationale": "Age-based screening recommendation"
174
+ })
175
+
176
+ # Hypertension screening
177
+ if age >= 18:
178
+ freq = "Annually" if age >= 40 else "Every 3-5 years"
179
+ rec = PREVENTIVE_GUIDELINES["hypertension_screening"][0].copy()
180
+ rec["frequency"] = freq
181
+ rec["applicable"] = True
182
+ rec["rationale"] = "Universal screening for adults"
183
+ recommendations.append(rec)
184
+
185
+ # Cholesterol/CVD screening
186
+ if 40 <= age <= 75:
187
+ recommendations.append({
188
+ **PREVENTIVE_GUIDELINES["cholesterol_screening"][0],
189
+ "applicable": True,
190
+ "rationale": "Age-based cardiovascular risk screening"
191
+ })
192
+
193
+ # Sex-specific screenings
194
+ if sex.lower() == "female":
195
+ if 40 <= age <= 74:
196
+ recommendations.append({
197
+ "title": "Breast Cancer Screening",
198
+ "recommendation": "Biennial screening mammography for women aged 40-74",
199
+ "grade": "B",
200
+ "age_range": "40-74 years",
201
+ "frequency": "Every 2 years",
202
+ "source": "USPSTF",
203
+ "applicable": True,
204
+ "citation": "USPSTF 2024"
205
+ })
206
+ if 21 <= age <= 65:
207
+ recommendations.append({
208
+ "title": "Cervical Cancer Screening",
209
+ "recommendation": "Screening every 3 years with cytology (21-29) or every 5 years with HPV testing (30-65)",
210
+ "grade": "A",
211
+ "age_range": "21-65 years",
212
+ "frequency": "Every 3-5 years based on method",
213
+ "source": "USPSTF",
214
+ "applicable": True,
215
+ "citation": "USPSTF 2018"
216
+ })
217
+ elif sex.lower() == "male":
218
+ if 55 <= age <= 80 and "smoking history" in condition_lower:
219
+ recommendations.append({
220
+ "title": "Lung Cancer Screening",
221
+ "recommendation": "Annual screening with low-dose CT for adults 50-80 with 20 pack-year smoking history",
222
+ "grade": "B",
223
+ "age_range": "50-80 years",
224
+ "frequency": "Annually",
225
+ "source": "USPSTF",
226
+ "applicable": True,
227
+ "conditions": ["20 pack-year smoking history"],
228
+ "citation": "USPSTF 2021"
229
+ })
230
+
231
+ return {
232
+ "patient_age": age,
233
+ "patient_sex": sex,
234
+ "conditions": conditions,
235
+ "recommendations": recommendations,
236
+ "total_recommendations": len(recommendations)
237
+ }
238
+
239
+ @safe_json_return
240
+ async def clinical_get_vaccine_schedule(
241
+ self,
242
+ age: int,
243
+ conditions: List[str] = None
244
+ ) -> Dict[str, Any]:
245
+ """
246
+ Get personalized vaccine recommendations based on CDC guidelines.
247
+
248
+ Args:
249
+ age: Patient age in years
250
+ conditions: List of health conditions
251
+
252
+ Returns:
253
+ Recommended vaccines with timing and priority
254
+ """
255
+ if conditions is None:
256
+ conditions = []
257
+
258
+ recommended_vaccines = []
259
+ condition_lower = [c.lower() for c in conditions]
260
+
261
+ # Universal vaccines
262
+ recommended_vaccines.append({
263
+ **VACCINE_SCHEDULE["influenza"],
264
+ "due": "Annually, preferably before flu season (Sept-Oct)",
265
+ "priority": "High" if age >= 65 or len(conditions) > 0 else "Standard"
266
+ })
267
+
268
+ recommended_vaccines.append({
269
+ **VACCINE_SCHEDULE["covid19"],
270
+ "due": "Check for updated formulation annually",
271
+ "priority": "High" if age >= 65 else "Standard"
272
+ })
273
+
274
+ # Age-specific vaccines
275
+ if age >= 65:
276
+ recommended_vaccines.append({
277
+ **VACCINE_SCHEDULE["pneumococcal"],
278
+ "due": "If not previously vaccinated",
279
+ "priority": "High"
280
+ })
281
+
282
+ if age >= 50:
283
+ recommended_vaccines.append({
284
+ **VACCINE_SCHEDULE["shingles"],
285
+ "due": "2-dose series if not previously vaccinated",
286
+ "priority": "High" if age >= 60 else "Standard"
287
+ })
288
+
289
+ # Condition-specific
290
+ if any(c in condition_lower for c in ["diabetes", "heart disease", "lung disease", "immunocompromised"]):
291
+ if age >= 19 and age < 65:
292
+ recommended_vaccines.append({
293
+ **VACCINE_SCHEDULE["pneumococcal"],
294
+ "due": "Based on risk factors",
295
+ "priority": "High",
296
+ "rationale": "Chronic medical condition"
297
+ })
298
+
299
+ # Tdap (universal adult)
300
+ recommended_vaccines.append({
301
+ **VACCINE_SCHEDULE["tdap"],
302
+ "due": "If not received in last 10 years",
303
+ "priority": "Standard"
304
+ })
305
+
306
+ # Hepatitis B
307
+ if 19 <= age <= 59:
308
+ recommended_vaccines.append({
309
+ **VACCINE_SCHEDULE["hepatitis_b"],
310
+ "due": "If not previously vaccinated",
311
+ "priority": "Standard"
312
+ })
313
+
314
+ return {
315
+ "patient_age": age,
316
+ "conditions": conditions,
317
+ "recommended_vaccines": recommended_vaccines,
318
+ "total_vaccines": len(recommended_vaccines)
319
+ }
320
+
321
+ @safe_json_return
322
+ async def clinical_search_guidelines(
323
+ self,
324
+ query: str,
325
+ source: str = "All"
326
+ ) -> Dict[str, Any]:
327
+ """
328
+ Search clinical guidelines by condition or screening type.
329
+
330
+ Args:
331
+ query: Search term (condition, screening type, etc.)
332
+ source: Filter by source (USPSTF, CDC, AHRQ, All)
333
+
334
+ Returns:
335
+ Matching guidelines with recommendations
336
+ """
337
+ query_lower = query.lower()
338
+ results = []
339
+
340
+ # Search preventive guidelines
341
+ for key, guidelines in PREVENTIVE_GUIDELINES.items():
342
+ if query_lower in key:
343
+ results.extend(guidelines)
344
+ else:
345
+ for g in guidelines:
346
+ if query_lower in g.get("title", "").lower():
347
+ results.append(g)
348
+
349
+ # Search vaccines
350
+ for key, vaccine in VACCINE_SCHEDULE.items():
351
+ if query_lower in key or query_lower in vaccine.get("vaccine", "").lower():
352
+ results.append({
353
+ "title": vaccine["vaccine"],
354
+ "recommendation": vaccine["recommendation"],
355
+ "source": vaccine["source"],
356
+ "type": "Immunization",
357
+ "age_groups": vaccine.get("age_groups", []),
358
+ "frequency": vaccine.get("frequency", "")
359
+ })
360
+
361
+ # Filter by source if specified
362
+ if source != "All":
363
+ results = [r for r in results if r.get("source", "").upper() == source.upper()]
364
+
365
+ return {
366
+ "query": query,
367
+ "source_filter": source,
368
+ "results": results,
369
+ "total": len(results)
370
+ }
371
+
372
+ @safe_json_return
373
+ async def clinical_get_lifestyle_recommendations(
374
+ self,
375
+ conditions: List[str],
376
+ age: int
377
+ ) -> Dict[str, Any]:
378
+ """
379
+ Get evidence-based lifestyle and wellness recommendations.
380
+
381
+ Args:
382
+ conditions: List of health conditions
383
+ age: Patient age
384
+
385
+ Returns:
386
+ Lifestyle modification recommendations
387
+ """
388
+ if conditions is None:
389
+ conditions = []
390
+
391
+ recommendations = []
392
+ condition_lower = [c.lower() for c in conditions]
393
+
394
+ # Universal recommendations
395
+ recommendations.append({
396
+ "category": "Physical Activity",
397
+ "recommendation": "At least 150 minutes of moderate-intensity aerobic activity per week",
398
+ "source": "CDC Physical Activity Guidelines",
399
+ "evidence_grade": "A"
400
+ })
401
+
402
+ recommendations.append({
403
+ "category": "Nutrition",
404
+ "recommendation": "Mediterranean-style diet rich in fruits, vegetables, whole grains, and healthy fats",
405
+ "source": "AHA Dietary Guidelines",
406
+ "evidence_grade": "A"
407
+ })
408
+
409
+ recommendations.append({
410
+ "category": "Sleep",
411
+ "recommendation": "7-9 hours of sleep per night for adults",
412
+ "source": "CDC Sleep Guidelines",
413
+ "evidence_grade": "A"
414
+ })
415
+
416
+ # Condition-specific
417
+ if "diabetes" in condition_lower:
418
+ recommendations.append({
419
+ "category": "Diabetes Management",
420
+ "recommendation": "Monitor blood glucose regularly, aim for HbA1c <7% for most adults",
421
+ "source": "ADA Standards of Care",
422
+ "evidence_grade": "A",
423
+ "specifics": ["Carbohydrate counting", "Regular foot examinations", "Annual eye exams"]
424
+ })
425
+
426
+ if "hypertension" in condition_lower:
427
+ recommendations.append({
428
+ "category": "Blood Pressure Management",
429
+ "recommendation": "DASH diet, sodium restriction (<2300mg/day), regular BP monitoring",
430
+ "source": "ACC/AHA Hypertension Guidelines",
431
+ "evidence_grade": "A",
432
+ "target": "BP <130/80 mmHg"
433
+ })
434
+
435
+ if any("heart" in c or "cardiac" in c or "cardiovascular" in c for c in condition_lower):
436
+ recommendations.append({
437
+ "category": "Cardiovascular Health",
438
+ "recommendation": "Cardiac rehabilitation if eligible, cholesterol management, smoking cessation",
439
+ "source": "AHA/ACC Guidelines",
440
+ "evidence_grade": "A"
441
+ })
442
+
443
+ if "obesity" in condition_lower or "overweight" in condition_lower:
444
+ recommendations.append({
445
+ "category": "Weight Management",
446
+ "recommendation": "5-10% weight loss through caloric reduction and increased physical activity",
447
+ "source": "USPSTF Obesity Guidelines",
448
+ "evidence_grade": "B"
449
+ })
450
+
451
+ return {
452
+ "conditions": conditions,
453
+ "patient_age": age,
454
+ "recommendations": recommendations,
455
+ "total": len(recommendations)
456
+ }
457
+
458
+ @safe_json_return
459
+ async def clinical_generate_care_plan(
460
+ self,
461
+ age: int,
462
+ sex: str,
463
+ conditions: List[str] = None,
464
+ current_medications: List[str] = None
465
+ ) -> Dict[str, Any]:
466
+ """
467
+ Generate comprehensive 6-month care plan with screening, vaccines, and lifestyle.
468
+
469
+ Args:
470
+ age: Patient age
471
+ sex: Patient sex
472
+ conditions: Existing conditions
473
+ current_medications: Current medication list
474
+
475
+ Returns:
476
+ Structured 6-month care plan
477
+ """
478
+ if conditions is None:
479
+ conditions = []
480
+ if current_medications is None:
481
+ current_medications = []
482
+
483
+ # Get all recommendation types
484
+ preventive = await self.clinical_get_preventive_recommendations(age, sex, conditions)
485
+ vaccines = await self.clinical_get_vaccine_schedule(age, conditions)
486
+ lifestyle = await self.clinical_get_lifestyle_recommendations(conditions, age)
487
+
488
+ # Build timeline
489
+ care_plan = {
490
+ "patient_profile": {
491
+ "age": age,
492
+ "sex": sex,
493
+ "conditions": conditions,
494
+ "current_medications": current_medications
495
+ },
496
+ "preventive_screenings": preventive.get("recommendations", []),
497
+ "immunizations": vaccines.get("recommended_vaccines", []),
498
+ "lifestyle_modifications": lifestyle.get("recommendations", []),
499
+ "timeline": {
500
+ "month_1": ["Schedule overdue screenings", "Update immunizations"],
501
+ "month_2": ["Follow up on screening results", "Begin lifestyle modifications"],
502
+ "month_3": ["Medication review with provider", "Assess lifestyle progress"],
503
+ "month_6": ["Comprehensive health review", "Update care plan"]
504
+ },
505
+ "follow_up_schedule": {
506
+ "primary_care": "Every 3-6 months" if conditions else "Annually",
507
+ "specialist": "As indicated by conditions" if conditions else "Not required",
508
+ "lab_work": "Per screening recommendations"
509
+ }
510
+ }
511
+
512
+ return care_plan
providers/cms_pricing_provider.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CMS Pricing Provider for Medicare fee schedules and cost transparency."""
2
+
3
+ import logging
4
+ from typing import List, Dict, Any, Callable
5
+ import httpx
6
+
7
+ from ..core.base_provider import BaseProvider
8
+ from ..core.decorators import safe_json_return
9
+ from . import register_provider
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Sample Medicare fee schedule data
14
+ MEDICARE_FEE_SCHEDULE = {
15
+ "99213": {
16
+ "description": "Office visit, established patient, level 3",
17
+ "medicare_payment": 93.47,
18
+ "facility_rate": 93.47,
19
+ "non_facility_rate": 131.20,
20
+ "rvu": 1.92
21
+ },
22
+ "99214": {
23
+ "description": "Office visit, established patient, level 4",
24
+ "medicare_payment": 131.20,
25
+ "facility_rate": 131.20,
26
+ "non_facility_rate": 183.19,
27
+ "rvu": 2.80
28
+ },
29
+ "43239": {
30
+ "description": "Upper GI endoscopy with biopsy",
31
+ "medicare_payment": 248.54,
32
+ "facility_rate": 248.54,
33
+ "non_facility_rate": 450.00,
34
+ "rvu": 5.28
35
+ },
36
+ "45378": {
37
+ "description": "Colonoscopy, diagnostic",
38
+ "medicare_payment": 365.00,
39
+ "facility_rate": 365.00,
40
+ "non_facility_rate": 650.00,
41
+ "rvu": 7.75
42
+ },
43
+ "70450": {
44
+ "description": "CT scan, head without contrast",
45
+ "medicare_payment": 178.00,
46
+ "facility_rate": 178.00,
47
+ "non_facility_rate": 320.00,
48
+ "rvu": 3.78
49
+ },
50
+ "70553": {
51
+ "description": "MRI brain with contrast",
52
+ "medicare_payment": 485.00,
53
+ "facility_rate": 485.00,
54
+ "non_facility_rate": 875.00,
55
+ "rvu": 10.30
56
+ },
57
+ "80053": {
58
+ "description": "Comprehensive metabolic panel",
59
+ "medicare_payment": 14.00,
60
+ "facility_rate": 14.00,
61
+ "non_facility_rate": 18.00,
62
+ "rvu": 0.30
63
+ },
64
+ "85025": {
65
+ "description": "Complete blood count with differential",
66
+ "medicare_payment": 11.00,
67
+ "facility_rate": 11.00,
68
+ "non_facility_rate": 14.00,
69
+ "rvu": 0.23
70
+ },
71
+ "71046": {
72
+ "description": "Chest X-ray, 2 views",
73
+ "medicare_payment": 38.00,
74
+ "facility_rate": 38.00,
75
+ "non_facility_rate": 62.00,
76
+ "rvu": 0.81
77
+ }
78
+ }
79
+
80
+ # Sample procedure cost estimates
81
+ PROCEDURE_COST_ESTIMATES = {
82
+ "colonoscopy": {
83
+ "procedure": "Diagnostic Colonoscopy",
84
+ "hcpcs_code": "45378",
85
+ "average_cost": 2750,
86
+ "low_estimate": 1500,
87
+ "high_estimate": 4500,
88
+ "medicare_rate": 365,
89
+ "factors": ["Facility type", "Anesthesia", "Biopsy performed", "Location"]
90
+ },
91
+ "mri": {
92
+ "procedure": "MRI Scan",
93
+ "hcpcs_code": "70553",
94
+ "average_cost": 1420,
95
+ "low_estimate": 400,
96
+ "high_estimate": 3500,
97
+ "medicare_rate": 485,
98
+ "factors": ["Body part", "With/without contrast", "Facility type"]
99
+ },
100
+ "ct scan": {
101
+ "procedure": "CT Scan",
102
+ "hcpcs_code": "70450",
103
+ "average_cost": 820,
104
+ "low_estimate": 300,
105
+ "high_estimate": 1800,
106
+ "medicare_rate": 178,
107
+ "factors": ["Body part", "With/without contrast", "Emergency vs scheduled"]
108
+ },
109
+ "blood test": {
110
+ "procedure": "Comprehensive Metabolic Panel",
111
+ "hcpcs_code": "80053",
112
+ "average_cost": 45,
113
+ "low_estimate": 15,
114
+ "high_estimate": 120,
115
+ "medicare_rate": 14,
116
+ "factors": ["Lab facility", "Stat vs routine", "Insurance negotiated rate"]
117
+ },
118
+ "x-ray": {
119
+ "procedure": "Chest X-Ray",
120
+ "hcpcs_code": "71046",
121
+ "average_cost": 125,
122
+ "low_estimate": 50,
123
+ "high_estimate": 300,
124
+ "medicare_rate": 38,
125
+ "factors": ["Number of views", "Facility type", "Interpretation included"]
126
+ },
127
+ "office visit": {
128
+ "procedure": "Office Visit - Established Patient",
129
+ "hcpcs_code": "99213",
130
+ "average_cost": 150,
131
+ "low_estimate": 100,
132
+ "high_estimate": 300,
133
+ "medicare_rate": 93,
134
+ "factors": ["Complexity level", "Time spent", "Geographic location"]
135
+ }
136
+ }
137
+
138
+ # Sample facility comparison data
139
+ FACILITY_COSTS = {
140
+ "45378": [ # Colonoscopy
141
+ {"facility": "University Hospital", "gross_charge": 4500, "negotiated_rate": 2800, "cash_price": 2500},
142
+ {"facility": "Community Medical Center", "gross_charge": 3200, "negotiated_rate": 2100, "cash_price": 1800},
143
+ {"facility": "Ambulatory Surgery Center", "gross_charge": 2800, "negotiated_rate": 1600, "cash_price": 1500},
144
+ ],
145
+ "70450": [ # CT Scan
146
+ {"facility": "University Hospital", "gross_charge": 1800, "negotiated_rate": 820, "cash_price": 600},
147
+ {"facility": "Community Medical Center", "gross_charge": 1200, "negotiated_rate": 650, "cash_price": 500},
148
+ {"facility": "Imaging Center", "gross_charge": 900, "negotiated_rate": 450, "cash_price": 400},
149
+ ],
150
+ "70553": [ # MRI
151
+ {"facility": "University Hospital", "gross_charge": 3500, "negotiated_rate": 1800, "cash_price": 1500},
152
+ {"facility": "Community Medical Center", "gross_charge": 2800, "negotiated_rate": 1400, "cash_price": 1200},
153
+ {"facility": "Imaging Center", "gross_charge": 2200, "negotiated_rate": 1000, "cash_price": 900},
154
+ ]
155
+ }
156
+
157
+ # Procedure code database
158
+ PROCEDURE_CODES = [
159
+ {"code": "99213", "description": "Office visit, established patient, level 3", "category": "Evaluation & Management"},
160
+ {"code": "99214", "description": "Office visit, established patient, level 4", "category": "Evaluation & Management"},
161
+ {"code": "99215", "description": "Office visit, established patient, level 5", "category": "Evaluation & Management"},
162
+ {"code": "99203", "description": "Office visit, new patient, level 3", "category": "Evaluation & Management"},
163
+ {"code": "45378", "description": "Colonoscopy, diagnostic", "category": "Gastroenterology"},
164
+ {"code": "43239", "description": "Upper GI endoscopy with biopsy", "category": "Gastroenterology"},
165
+ {"code": "70450", "description": "CT scan, head without contrast", "category": "Radiology"},
166
+ {"code": "70553", "description": "MRI brain with contrast", "category": "Radiology"},
167
+ {"code": "71046", "description": "Chest X-ray, 2 views", "category": "Radiology"},
168
+ {"code": "80053", "description": "Comprehensive metabolic panel", "category": "Laboratory"},
169
+ {"code": "85025", "description": "Complete blood count with differential", "category": "Laboratory"},
170
+ {"code": "82947", "description": "Glucose blood test", "category": "Laboratory"},
171
+ {"code": "83036", "description": "Hemoglobin A1C", "category": "Laboratory"},
172
+ ]
173
+
174
+
175
+ @register_provider("cms_pricing")
176
+ class CMSPricingProvider(BaseProvider):
177
+ """Provider for Medicare pricing and cost transparency data."""
178
+
179
+ def __init__(self, client: httpx.AsyncClient):
180
+ super().__init__("cms_pricing", client)
181
+
182
+ async def initialize(self) -> None:
183
+ """Initialize CMS pricing provider."""
184
+ logger.info("CMS Pricing provider initialized")
185
+
186
+ def get_tools(self) -> List[Callable]:
187
+ """Return all CMS pricing tools."""
188
+ return [
189
+ self.cms_get_medicare_fee,
190
+ self.cms_estimate_procedure_cost,
191
+ self.cms_search_procedure_codes,
192
+ self.cms_compare_facility_costs,
193
+ self.cms_estimate_out_of_pocket,
194
+ ]
195
+
196
+ @safe_json_return
197
+ async def cms_get_medicare_fee(
198
+ self,
199
+ hcpcs_code: str,
200
+ locality: str = "National"
201
+ ) -> Dict[str, Any]:
202
+ """
203
+ Get Medicare fee schedule pricing for a procedure by HCPCS/CPT code.
204
+
205
+ Args:
206
+ hcpcs_code: HCPCS procedure code (e.g., "99213", "43239")
207
+ locality: Geographic locality (default: "National")
208
+
209
+ Returns:
210
+ Medicare reimbursement rates with facility vs non-facility pricing
211
+ """
212
+ result = MEDICARE_FEE_SCHEDULE.get(hcpcs_code, {
213
+ "description": f"Procedure {hcpcs_code}",
214
+ "medicare_payment": 150.00,
215
+ "facility_rate": 150.00,
216
+ "non_facility_rate": 200.00,
217
+ "rvu": 3.0,
218
+ "note": "Sample data - actual rates vary by locality and year"
219
+ })
220
+
221
+ result["hcpcs_code"] = hcpcs_code
222
+ result["locality"] = locality
223
+ result["year"] = 2024
224
+
225
+ return result
226
+
227
+ @safe_json_return
228
+ async def cms_estimate_procedure_cost(
229
+ self,
230
+ procedure_name: str,
231
+ region: str = "National",
232
+ insurance_type: str = "Medicare"
233
+ ) -> Dict[str, Any]:
234
+ """
235
+ Estimate typical cost range for a procedure in a geographic region.
236
+
237
+ Args:
238
+ procedure_name: Common procedure name (e.g., "colonoscopy", "MRI", "blood test")
239
+ region: Geographic region (default: "National")
240
+ insurance_type: Type of insurance (Medicare, Commercial, Cash)
241
+
242
+ Returns:
243
+ Estimated cost range with average, low, and high estimates
244
+ """
245
+ procedure_key = procedure_name.lower()
246
+ result = PROCEDURE_COST_ESTIMATES.get(procedure_key, {
247
+ "procedure": procedure_name.title(),
248
+ "hcpcs_code": "Unknown",
249
+ "average_cost": 500,
250
+ "low_estimate": 200,
251
+ "high_estimate": 1000,
252
+ "medicare_rate": 150,
253
+ "factors": ["Procedure complexity", "Facility type", "Geographic location"]
254
+ })
255
+
256
+ result["region"] = region
257
+ result["insurance_type"] = insurance_type
258
+ result["note"] = "Estimates based on national averages. Actual costs vary significantly by provider and location."
259
+
260
+ # Adjust for insurance type
261
+ if insurance_type.lower() == "commercial":
262
+ result["estimated_patient_cost"] = result["average_cost"]
263
+ elif insurance_type.lower() == "medicare":
264
+ result["estimated_patient_cost"] = result["medicare_rate"] * 1.2 # 20% coinsurance
265
+ elif insurance_type.lower() == "cash":
266
+ result["estimated_patient_cost"] = result["average_cost"] * 0.7 # Cash discount
267
+
268
+ return result
269
+
270
+ @safe_json_return
271
+ async def cms_search_procedure_codes(
272
+ self,
273
+ query: str,
274
+ limit: int = 10
275
+ ) -> Dict[str, Any]:
276
+ """
277
+ Search for HCPCS/CPT codes by procedure description or category.
278
+
279
+ Args:
280
+ query: Search term (e.g., "office visit", "colonoscopy")
281
+ limit: Maximum number of results (default: 10)
282
+
283
+ Returns:
284
+ List of matching procedures with codes, descriptions, and categories
285
+ """
286
+ query_lower = query.lower()
287
+ matches = [
288
+ p for p in PROCEDURE_CODES
289
+ if query_lower in p["description"].lower() or query_lower in p["category"].lower()
290
+ ]
291
+
292
+ return {
293
+ "query": query,
294
+ "results": matches[:limit],
295
+ "total": len(matches)
296
+ }
297
+
298
+ @safe_json_return
299
+ async def cms_compare_facility_costs(
300
+ self,
301
+ procedure_code: str,
302
+ facilities: List[str] = None
303
+ ) -> Dict[str, Any]:
304
+ """
305
+ Compare procedure costs across different healthcare facilities.
306
+
307
+ Args:
308
+ procedure_code: HCPCS/CPT code
309
+ facilities: List of facility names (uses sample data if not provided)
310
+
311
+ Returns:
312
+ Cost comparison showing gross charges, negotiated rates, and cash prices
313
+ """
314
+ facilities_data = FACILITY_COSTS.get(procedure_code, [
315
+ {"facility": "Sample Facility A", "gross_charge": 2000, "negotiated_rate": 1200, "cash_price": 1000},
316
+ {"facility": "Sample Facility B", "gross_charge": 1500, "negotiated_rate": 900, "cash_price": 800},
317
+ {"facility": "Sample Facility C", "gross_charge": 1800, "negotiated_rate": 1000, "cash_price": 900},
318
+ ])
319
+
320
+ # Calculate savings
321
+ cash_prices = [f["cash_price"] for f in facilities_data]
322
+ lowest_cash = min(cash_prices)
323
+ highest_cash = max(cash_prices)
324
+ potential_savings = highest_cash - lowest_cash
325
+
326
+ return {
327
+ "procedure_code": procedure_code,
328
+ "facilities": facilities_data,
329
+ "comparison": {
330
+ "lowest_cash_price": lowest_cash,
331
+ "highest_cash_price": highest_cash,
332
+ "potential_savings": potential_savings,
333
+ "average_cash_price": sum(cash_prices) / len(cash_prices)
334
+ },
335
+ "note": "Sample data for demonstration. Actual facility prices vary. Patients should request price estimates directly."
336
+ }
337
+
338
+ @safe_json_return
339
+ async def cms_estimate_out_of_pocket(
340
+ self,
341
+ procedure_code: str,
342
+ insurance_scenario: str = "Medicare",
343
+ deductible_met: bool = False
344
+ ) -> Dict[str, Any]:
345
+ """
346
+ Estimate patient out-of-pocket costs for common insurance scenarios.
347
+
348
+ Args:
349
+ procedure_code: HCPCS/CPT code
350
+ insurance_scenario: Insurance type (Medicare, Commercial High Deductible, Commercial PPO, Cash)
351
+ deductible_met: Whether annual deductible has been met
352
+
353
+ Returns:
354
+ Estimated patient financial responsibility
355
+ """
356
+ # Get base Medicare cost
357
+ medicare_data = await self.cms_get_medicare_fee(procedure_code)
358
+ base_cost = medicare_data.get("medicare_payment", 200)
359
+
360
+ # Define insurance scenarios
361
+ scenarios = {
362
+ "Medicare": {
363
+ "coinsurance": 0.20,
364
+ "copay": 0,
365
+ "description": "Medicare Part B (20% coinsurance after deductible)",
366
+ "deductible": 240 # 2024 Part B deductible
367
+ },
368
+ "Commercial PPO": {
369
+ "coinsurance": 0.20,
370
+ "copay": 0,
371
+ "description": "Commercial PPO (20% coinsurance after deductible)",
372
+ "deductible": 2000,
373
+ "multiplier": 2.5 # Commercial pays ~2.5x Medicare
374
+ },
375
+ "Commercial High Deductible": {
376
+ "coinsurance": 0.30,
377
+ "copay": 0,
378
+ "description": "High Deductible Health Plan (30% coinsurance)",
379
+ "deductible": 5000,
380
+ "multiplier": 2.5
381
+ },
382
+ "Cash": {
383
+ "coinsurance": 0,
384
+ "copay": 0,
385
+ "description": "Cash/Self-pay (negotiated rate)",
386
+ "deductible": 0,
387
+ "multiplier": 1.8 # Cash discount from commercial
388
+ }
389
+ }
390
+
391
+ scenario = scenarios.get(insurance_scenario, scenarios["Medicare"])
392
+
393
+ # Calculate costs
394
+ allowed_amount = base_cost * scenario.get("multiplier", 1.0)
395
+
396
+ if deductible_met:
397
+ patient_responsibility = allowed_amount * scenario["coinsurance"] + scenario["copay"]
398
+ else:
399
+ # Patient pays deductible first, then coinsurance on remainder
400
+ if allowed_amount <= scenario["deductible"]:
401
+ patient_responsibility = allowed_amount
402
+ else:
403
+ remaining = allowed_amount - scenario["deductible"]
404
+ patient_responsibility = scenario["deductible"] + (remaining * scenario["coinsurance"])
405
+
406
+ return {
407
+ "procedure_code": procedure_code,
408
+ "procedure_description": medicare_data.get("description", "Unknown procedure"),
409
+ "insurance_scenario": insurance_scenario,
410
+ "deductible_met": deductible_met,
411
+ "cost_breakdown": {
412
+ "allowed_amount": round(allowed_amount, 2),
413
+ "deductible": scenario["deductible"],
414
+ "coinsurance_rate": f"{int(scenario['coinsurance'] * 100)}%",
415
+ "estimated_patient_responsibility": round(patient_responsibility, 2),
416
+ "insurance_pays": round(allowed_amount - patient_responsibility, 2)
417
+ },
418
+ "scenario_details": scenario["description"],
419
+ "note": "Estimates only. Actual costs depend on specific plan benefits and negotiated rates."
420
+ }
providers/openfda_provider.py ADDED
@@ -0,0 +1,573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenFDA API Provider for adverse events, drug labels, recalls, and more."""
2
+
3
+ import re
4
+ import logging
5
+ from typing import List, Dict, Any, Callable, Optional
6
+ import httpx
7
+ from functools import lru_cache
8
+
9
+ from ..core.base_provider import BaseProvider
10
+ from ..core.decorators import safe_json_return, with_retry
11
+ from . import register_provider
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # OpenFDA API endpoints
16
+ OPENFDA_DRUG_EVENT = "https://api.fda.gov/drug/event.json"
17
+ OPENFDA_DRUG_LABEL = "https://api.fda.gov/drug/label.json"
18
+ OPENFDA_DRUG_NDC = "https://api.fda.gov/drug/ndc.json"
19
+ OPENFDA_DRUG_ENFORCEMENT = "https://api.fda.gov/drug/enforcement.json"
20
+ OPENFDA_DRUG_DRUGSFDA = "https://api.fda.gov/drug/drugsfda.json"
21
+ OPENFDA_DEVICE_EVENT = "https://api.fda.gov/device/event.json"
22
+ OPENFDA_DEVICE_ENFORCEMENT = "https://api.fda.gov/device/enforcement.json"
23
+ OPENFDA_DEVICE_CLASSIFICATION = "https://api.fda.gov/device/classification.json"
24
+ OPENFDA_DEVICE_510K = "https://api.fda.gov/device/510k.json"
25
+ OPENFDA_DEVICE_PMA = "https://api.fda.gov/device/pma.json"
26
+ OPENFDA_FOOD_ENFORCEMENT = "https://api.fda.gov/food/enforcement.json"
27
+ OPENFDA_FOOD_EVENT = "https://api.fda.gov/food/event.json"
28
+
29
+
30
+ def normalize_drug_name(raw_name: str) -> str:
31
+ """Extract base drug name from full medication name."""
32
+ if not raw_name:
33
+ return ""
34
+
35
+ cleaned = raw_name
36
+ patterns = [
37
+ r'\s+\d+(?:\.\d+)?\s*(MG|MCG|ML|G|%)',
38
+ r'\s+Oral\s+',
39
+ r'\s+Tablet\s*',
40
+ r'\s+Capsule\s*',
41
+ r'\s+Injectable\s*',
42
+ r'\s+Solution\s*',
43
+ r'\s+Suspension\s*'
44
+ ]
45
+
46
+ for pattern in patterns:
47
+ cleaned = re.split(pattern, cleaned, flags=re.IGNORECASE)[0]
48
+
49
+ base_name = cleaned.strip().split()[0] if cleaned.strip() else cleaned.strip()
50
+ return base_name
51
+
52
+
53
+ @register_provider("openfda")
54
+ class OpenFDAProvider(BaseProvider):
55
+ """Provider for OpenFDA APIs."""
56
+
57
+ def __init__(self, client: httpx.AsyncClient, api_key: Optional[str] = None):
58
+ super().__init__("openfda", client)
59
+ self.api_key = api_key
60
+
61
+ async def initialize(self) -> None:
62
+ """Initialize OpenFDA provider."""
63
+ logger.info("OpenFDA provider initialized")
64
+
65
+ def get_tools(self) -> List[Callable]:
66
+ """Return all OpenFDA tools."""
67
+ return [
68
+ self.openfda_get_adverse_event_summary,
69
+ self.openfda_fetch_adverse_events,
70
+ self.openfda_top_reactions,
71
+ self.openfda_search_drug_labels,
72
+ self.openfda_search_ndc,
73
+ self.openfda_search_drug_recalls,
74
+ self.openfda_search_drugs_fda,
75
+ self.openfda_search_device_events,
76
+ self.openfda_search_device_recalls,
77
+ self.openfda_search_device_classifications,
78
+ self.openfda_search_510k,
79
+ self.openfda_search_pma,
80
+ self.openfda_search_food_recalls,
81
+ self.openfda_search_food_events,
82
+ ]
83
+
84
+ def _build_params(self, search: str, limit: int = 10) -> Dict[str, Any]:
85
+ """Build query parameters with optional API key."""
86
+ params = {"search": search, "limit": min(limit, 100)}
87
+ if self.api_key:
88
+ params["api_key"] = self.api_key
89
+ return params
90
+
91
+ @with_retry
92
+ async def _fetch_fda_data(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
93
+ """Fetch data from OpenFDA API with retry logic."""
94
+ response = await self.client.get(url, params=params)
95
+ response.raise_for_status()
96
+ return response.json()
97
+
98
+ # Drug Adverse Events
99
+ @safe_json_return
100
+ async def openfda_get_adverse_event_summary(self, drug_name: str) -> Dict[str, Any]:
101
+ """
102
+ Get high-level adverse event summary for a medication from FAERS database.
103
+
104
+ Args:
105
+ drug_name: Name of the medication (generic or brand name)
106
+
107
+ Returns:
108
+ Summary with total reports, serious reports, and top reactions
109
+ """
110
+ clean_name = normalize_drug_name(drug_name)
111
+ logger.info(f"FDA adverse event query: '{drug_name}' -> '{clean_name}'")
112
+
113
+ # Query for count and reactions
114
+ search_query = f'patient.drug.medicinalproduct:"{clean_name}"'
115
+ params = self._build_params(search_query, limit=1)
116
+ params["count"] = "patient.reaction.reactionmeddrapt.exact"
117
+
118
+ data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, params)
119
+
120
+ total_reports = data.get("meta", {}).get("results", {}).get("total", 0)
121
+ reactions = data.get("results", [])[:5]
122
+
123
+ top_reactions = [
124
+ {"reaction": r.get("term", "Unknown"), "count": r.get("count", 0)}
125
+ for r in reactions
126
+ ]
127
+
128
+ # Get serious event count
129
+ serious_query = f'{search_query}+AND+serious:1'
130
+ serious_params = self._build_params(serious_query, limit=1)
131
+ try:
132
+ serious_data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, serious_params)
133
+ serious_reports = serious_data.get("meta", {}).get("results", {}).get("total", 0)
134
+ except Exception:
135
+ serious_reports = 0
136
+
137
+ return {
138
+ "drug": clean_name,
139
+ "total_reports": total_reports,
140
+ "serious_reports": serious_reports,
141
+ "top_reactions": top_reactions
142
+ }
143
+
144
+ @safe_json_return
145
+ async def openfda_fetch_adverse_events(
146
+ self,
147
+ drug_name: str,
148
+ limit: int = 25,
149
+ max_pages: int = 1
150
+ ) -> Dict[str, Any]:
151
+ """
152
+ Fetch raw adverse event reports from OpenFDA with pagination.
153
+
154
+ Args:
155
+ drug_name: Name of the medication
156
+ limit: Number of events per page (max 100)
157
+ max_pages: Number of pages to fetch
158
+
159
+ Returns:
160
+ List of adverse event reports with metadata
161
+ """
162
+ clean_name = normalize_drug_name(drug_name)
163
+ search_query = f'patient.drug.medicinalproduct:"{clean_name}"'
164
+
165
+ all_events = []
166
+ for page in range(max_pages):
167
+ params = self._build_params(search_query, limit=min(limit, 100))
168
+ params["skip"] = page * limit
169
+
170
+ try:
171
+ data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, params)
172
+ results = data.get("results", [])
173
+
174
+ for result in results:
175
+ reactions = result.get("patient", {}).get("reaction", [])
176
+ reaction_list = [r.get("reactionmeddrapt", "Unknown") for r in reactions]
177
+
178
+ all_events.append({
179
+ "safety_report_id": result.get("safetyreportid", "Unknown"),
180
+ "receive_date": result.get("receivedate", "Unknown"),
181
+ "serious": result.get("serious", 0) == 1,
182
+ "reactions": reaction_list[:5],
183
+ "patient_age": result.get("patient", {}).get("patientonsetage", "Unknown"),
184
+ "patient_sex": result.get("patient", {}).get("patientsex", "Unknown")
185
+ })
186
+
187
+ if len(results) < limit:
188
+ break
189
+ except Exception as e:
190
+ logger.warning(f"Error fetching page {page}: {e}")
191
+ break
192
+
193
+ return {
194
+ "drug": clean_name,
195
+ "events": all_events,
196
+ "total_fetched": len(all_events)
197
+ }
198
+
199
+ @safe_json_return
200
+ async def openfda_top_reactions(self, drug_name: str) -> Dict[str, Any]:
201
+ """
202
+ Get top 5 most commonly reported adverse reactions for a medication.
203
+
204
+ Args:
205
+ drug_name: Name of the medication
206
+
207
+ Returns:
208
+ Top 5 reactions with counts
209
+ """
210
+ clean_name = normalize_drug_name(drug_name)
211
+ search_query = f'patient.drug.medicinalproduct:"{clean_name}"'
212
+
213
+ params = self._build_params(search_query, limit=1)
214
+ params["count"] = "patient.reaction.reactionmeddrapt.exact"
215
+
216
+ data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, params)
217
+ reactions = data.get("results", [])[:5]
218
+
219
+ return {
220
+ "drug": clean_name,
221
+ "top_reactions": [
222
+ {"reaction": r.get("term", "Unknown"), "count": r.get("count", 0)}
223
+ for r in reactions
224
+ ]
225
+ }
226
+
227
+ # Drug Labels
228
+ @safe_json_return
229
+ async def openfda_search_drug_labels(self, query: str, limit: int = 10) -> Dict[str, Any]:
230
+ """
231
+ Search FDA drug labeling information (warnings, indications, dosage).
232
+
233
+ Args:
234
+ query: Drug name, active ingredient, or condition
235
+ limit: Maximum results (max 100)
236
+
237
+ Returns:
238
+ Drug label information
239
+ """
240
+ params = self._build_params(query, limit)
241
+ data = await self._fetch_fda_data(OPENFDA_DRUG_LABEL, params)
242
+
243
+ results = data.get("results", [])
244
+ labels = []
245
+
246
+ for result in results:
247
+ openfda = result.get("openfda", {})
248
+ labels.append({
249
+ "brand_name": openfda.get("brand_name", ["Unknown"])[0],
250
+ "generic_name": openfda.get("generic_name", ["Unknown"])[0],
251
+ "manufacturer": openfda.get("manufacturer_name", ["Unknown"])[0],
252
+ "purpose": result.get("purpose", ["Not specified"])[0] if result.get("purpose") else "Not specified",
253
+ "warnings": result.get("warnings", ["Not specified"])[0][:500] if result.get("warnings") else "Not specified",
254
+ "indications_and_usage": result.get("indications_and_usage", ["Not specified"])[0][:500] if result.get("indications_and_usage") else "Not specified"
255
+ })
256
+
257
+ return {"results": labels, "total": len(labels)}
258
+
259
+ # NDC Directory
260
+ @safe_json_return
261
+ async def openfda_search_ndc(self, query: str, limit: int = 10) -> Dict[str, Any]:
262
+ """
263
+ Search National Drug Code (NDC) directory for drug product information.
264
+
265
+ Args:
266
+ query: Brand name, generic name, or NDC number
267
+ limit: Maximum results (max 100)
268
+
269
+ Returns:
270
+ NDC product information
271
+ """
272
+ params = self._build_params(query, limit)
273
+ data = await self._fetch_fda_data(OPENFDA_DRUG_NDC, params)
274
+
275
+ results = data.get("results", [])
276
+ products = []
277
+
278
+ for result in results:
279
+ products.append({
280
+ "product_ndc": result.get("product_ndc", "Unknown"),
281
+ "brand_name": result.get("brand_name", "Unknown"),
282
+ "generic_name": result.get("generic_name", "Unknown"),
283
+ "manufacturer": result.get("labeler_name", "Unknown"),
284
+ "dosage_form": result.get("dosage_form", "Unknown"),
285
+ "route": result.get("route", ["Unknown"])[0] if result.get("route") else "Unknown",
286
+ "marketing_status": result.get("marketing_status", "Unknown")
287
+ })
288
+
289
+ return {"results": products, "total": len(products)}
290
+
291
+ # Drug Recalls
292
+ @safe_json_return
293
+ async def openfda_search_drug_recalls(self, query: str, limit: int = 10) -> Dict[str, Any]:
294
+ """
295
+ Search FDA drug recall and enforcement reports.
296
+
297
+ Args:
298
+ query: Drug name, manufacturer, or reason for recall
299
+ limit: Maximum results (max 100)
300
+
301
+ Returns:
302
+ Drug recall information
303
+ """
304
+ params = self._build_params(query, limit)
305
+ data = await self._fetch_fda_data(OPENFDA_DRUG_ENFORCEMENT, params)
306
+
307
+ results = data.get("results", [])
308
+ recalls = []
309
+
310
+ for result in results:
311
+ recalls.append({
312
+ "product_description": result.get("product_description", "Unknown"),
313
+ "reason_for_recall": result.get("reason_for_recall", "Unknown"),
314
+ "classification": result.get("classification", "Unknown"),
315
+ "status": result.get("status", "Unknown"),
316
+ "recall_date": result.get("recall_initiation_date", "Unknown"),
317
+ "recalling_firm": result.get("recalling_firm", "Unknown")
318
+ })
319
+
320
+ return {"results": recalls, "total": len(recalls)}
321
+
322
+ # Drugs@FDA
323
+ @safe_json_return
324
+ async def openfda_search_drugs_fda(self, query: str, limit: int = 10) -> Dict[str, Any]:
325
+ """
326
+ Search Drugs@FDA database for approved drug products and applications.
327
+
328
+ Args:
329
+ query: Drug name or active ingredient
330
+ limit: Maximum results (max 100)
331
+
332
+ Returns:
333
+ Approved drug information
334
+ """
335
+ params = self._build_params(query, limit)
336
+ data = await self._fetch_fda_data(OPENFDA_DRUG_DRUGSFDA, params)
337
+
338
+ results = data.get("results", [])
339
+ drugs = []
340
+
341
+ for result in results:
342
+ products = result.get("products", [])
343
+ for product in products:
344
+ drugs.append({
345
+ "application_number": result.get("application_number", "Unknown"),
346
+ "sponsor_name": result.get("sponsor_name", "Unknown"),
347
+ "brand_name": product.get("brand_name", "Unknown"),
348
+ "active_ingredients": product.get("active_ingredients", []),
349
+ "dosage_form": product.get("dosage_form", "Unknown"),
350
+ "route": product.get("route", "Unknown"),
351
+ "marketing_status": product.get("marketing_status", "Unknown")
352
+ })
353
+
354
+ return {"results": drugs, "total": len(drugs)}
355
+
356
+ # Device Events
357
+ @safe_json_return
358
+ async def openfda_search_device_events(self, query: str, limit: int = 10) -> Dict[str, Any]:
359
+ """
360
+ Search medical device adverse event reports.
361
+
362
+ Args:
363
+ query: Device name or brand
364
+ limit: Maximum results (max 100)
365
+
366
+ Returns:
367
+ Device adverse event information
368
+ """
369
+ params = self._build_params(query, limit)
370
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_EVENT, params)
371
+
372
+ results = data.get("results", [])
373
+ events = []
374
+
375
+ for result in results:
376
+ device = result.get("device", [{}])[0]
377
+ events.append({
378
+ "report_number": result.get("report_number", "Unknown"),
379
+ "date_received": result.get("date_received", "Unknown"),
380
+ "device_name": device.get("brand_name", "Unknown"),
381
+ "manufacturer": device.get("manufacturer_d_name", "Unknown"),
382
+ "event_type": result.get("event_type", "Unknown"),
383
+ "device_problem": device.get("device_problem_codes", ["Unknown"])[0] if device.get("device_problem_codes") else "Unknown"
384
+ })
385
+
386
+ return {"results": events, "total": len(events)}
387
+
388
+ # Device Recalls
389
+ @safe_json_return
390
+ async def openfda_search_device_recalls(self, query: str, limit: int = 10) -> Dict[str, Any]:
391
+ """
392
+ Search medical device recall and enforcement reports.
393
+
394
+ Args:
395
+ query: Device name, manufacturer, or reason
396
+ limit: Maximum results (max 100)
397
+
398
+ Returns:
399
+ Device recall information
400
+ """
401
+ params = self._build_params(query, limit)
402
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_ENFORCEMENT, params)
403
+
404
+ results = data.get("results", [])
405
+ recalls = []
406
+
407
+ for result in results:
408
+ recalls.append({
409
+ "product_description": result.get("product_description", "Unknown"),
410
+ "reason_for_recall": result.get("reason_for_recall", "Unknown"),
411
+ "classification": result.get("classification", "Unknown"),
412
+ "status": result.get("status", "Unknown"),
413
+ "recall_date": result.get("recall_initiation_date", "Unknown"),
414
+ "recalling_firm": result.get("recalling_firm", "Unknown")
415
+ })
416
+
417
+ return {"results": recalls, "total": len(recalls)}
418
+
419
+ # Device Classifications
420
+ @safe_json_return
421
+ async def openfda_search_device_classifications(self, query: str, limit: int = 10) -> Dict[str, Any]:
422
+ """
423
+ Search medical device classification database.
424
+
425
+ Args:
426
+ query: Device type or classification
427
+ limit: Maximum results (max 100)
428
+
429
+ Returns:
430
+ Device classification information
431
+ """
432
+ params = self._build_params(query, limit)
433
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_CLASSIFICATION, params)
434
+
435
+ results = data.get("results", [])
436
+ classifications = []
437
+
438
+ for result in results:
439
+ classifications.append({
440
+ "device_name": result.get("device_name", "Unknown"),
441
+ "device_class": result.get("device_class", "Unknown"),
442
+ "medical_specialty": result.get("medical_specialty_description", "Unknown"),
443
+ "regulation_number": result.get("regulation_number", "Unknown"),
444
+ "product_code": result.get("product_code", "Unknown")
445
+ })
446
+
447
+ return {"results": classifications, "total": len(classifications)}
448
+
449
+ # 510(k) Clearances
450
+ @safe_json_return
451
+ async def openfda_search_510k(self, query: str, limit: int = 10) -> Dict[str, Any]:
452
+ """
453
+ Search FDA 510(k) premarket clearance database.
454
+
455
+ Args:
456
+ query: Device name or manufacturer
457
+ limit: Maximum results (max 100)
458
+
459
+ Returns:
460
+ 510(k) clearance information
461
+ """
462
+ params = self._build_params(query, limit)
463
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_510K, params)
464
+
465
+ results = data.get("results", [])
466
+ clearances = []
467
+
468
+ for result in results:
469
+ clearances.append({
470
+ "k_number": result.get("k_number", "Unknown"),
471
+ "device_name": result.get("device_name", "Unknown"),
472
+ "applicant": result.get("applicant", "Unknown"),
473
+ "clearance_date": result.get("date_received", "Unknown"),
474
+ "decision_description": result.get("decision_description", "Unknown"),
475
+ "product_code": result.get("product_code", "Unknown")
476
+ })
477
+
478
+ return {"results": clearances, "total": len(clearances)}
479
+
480
+ # PMA Approvals
481
+ @safe_json_return
482
+ async def openfda_search_pma(self, query: str, limit: int = 10) -> Dict[str, Any]:
483
+ """
484
+ Search FDA premarket approval (PMA) database.
485
+
486
+ Args:
487
+ query: Device name or manufacturer
488
+ limit: Maximum results (max 100)
489
+
490
+ Returns:
491
+ PMA approval information
492
+ """
493
+ params = self._build_params(query, limit)
494
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_PMA, params)
495
+
496
+ results = data.get("results", [])
497
+ approvals = []
498
+
499
+ for result in results:
500
+ approvals.append({
501
+ "pma_number": result.get("pma_number", "Unknown"),
502
+ "device_name": result.get("device_name", "Unknown"),
503
+ "applicant": result.get("applicant", "Unknown"),
504
+ "approval_date": result.get("date_received", "Unknown"),
505
+ "decision_description": result.get("decision_description", "Unknown"),
506
+ "product_code": result.get("product_code", "Unknown")
507
+ })
508
+
509
+ return {"results": approvals, "total": len(approvals)}
510
+
511
+ # Food Recalls
512
+ @safe_json_return
513
+ async def openfda_search_food_recalls(self, query: str, limit: int = 10) -> Dict[str, Any]:
514
+ """
515
+ Search FDA food recall and enforcement reports.
516
+
517
+ Args:
518
+ query: Food product or reason for recall
519
+ limit: Maximum results (max 100)
520
+
521
+ Returns:
522
+ Food recall information
523
+ """
524
+ params = self._build_params(query, limit)
525
+ data = await self._fetch_fda_data(OPENFDA_FOOD_ENFORCEMENT, params)
526
+
527
+ results = data.get("results", [])
528
+ recalls = []
529
+
530
+ for result in results:
531
+ recalls.append({
532
+ "product_description": result.get("product_description", "Unknown"),
533
+ "reason_for_recall": result.get("reason_for_recall", "Unknown"),
534
+ "classification": result.get("classification", "Unknown"),
535
+ "status": result.get("status", "Unknown"),
536
+ "recall_date": result.get("recall_initiation_date", "Unknown"),
537
+ "recalling_firm": result.get("recalling_firm", "Unknown")
538
+ })
539
+
540
+ return {"results": recalls, "total": len(recalls)}
541
+
542
+ # Food Events
543
+ @safe_json_return
544
+ async def openfda_search_food_events(self, query: str, limit: int = 10) -> Dict[str, Any]:
545
+ """
546
+ Search FDA food adverse event reports.
547
+
548
+ Args:
549
+ query: Food product or reaction
550
+ limit: Maximum results (max 100)
551
+
552
+ Returns:
553
+ Food adverse event information
554
+ """
555
+ params = self._build_params(query, limit)
556
+ data = await self._fetch_fda_data(OPENFDA_FOOD_EVENT, params)
557
+
558
+ results = data.get("results", [])
559
+ events = []
560
+
561
+ for result in results:
562
+ products = result.get("products", [{}])
563
+ reactions = result.get("reactions", [])
564
+
565
+ events.append({
566
+ "report_number": result.get("report_number", "Unknown"),
567
+ "date_started": result.get("date_started", "Unknown"),
568
+ "products": [p.get("name_brand", "Unknown") for p in products],
569
+ "reactions": [r.get("reaction", "Unknown") for r in reactions][:5],
570
+ "outcomes": result.get("outcomes", ["Unknown"])
571
+ })
572
+
573
+ return {"results": events, "total": len(events)}
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ mcp>=1.0.0
2
+ httpx>=0.28.0
3
+ pydantic>=2.11.0
4
+ tenacity>=9.0.0
5
+ fastapi>=0.104.0
6
+ uvicorn[standard]>=0.24.0
server.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Healthcare API MCP Server - Public APIs for FDA, Clinical Guidelines, and CMS."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import List, Dict, Any
6
+
7
+ from mcp.server import Server
8
+ from mcp.server.stdio import stdio_server
9
+ from mcp.types import Tool, TextContent
10
+
11
+ from .config import MCPConfig
12
+ from .core import create_http_client
13
+ from .providers import get_provider_class
14
+ from .providers.openfda_provider import OpenFDAProvider
15
+ from .providers.clinical_guidelines_provider import ClinicalGuidelinesProvider
16
+ from .providers.cms_pricing_provider import CMSPricingProvider
17
+
18
+ # Setup logging
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
22
+ )
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Initialize MCP server
26
+ app = Server("healthcare-api-mcp")
27
+
28
+ # Global state
29
+ providers = {}
30
+ http_client = None
31
+
32
+
33
+ def create_tool_schema(func: callable, provider_name: str) -> Tool:
34
+ """Create MCP tool schema from provider function."""
35
+ # Get function metadata
36
+ doc = func.__doc__ or ""
37
+ lines = [line.strip() for line in doc.split('\n') if line.strip()]
38
+
39
+ # Extract description (first non-empty line)
40
+ description = lines[0] if lines else func.__name__
41
+
42
+ # Extract parameter info from docstring
43
+ # Parse Args section
44
+ params_start = None
45
+ params_end = None
46
+ for i, line in enumerate(lines):
47
+ if line.startswith('Args:'):
48
+ params_start = i + 1
49
+ elif params_start and line.startswith('Returns:'):
50
+ params_end = i
51
+ break
52
+
53
+ # Build input schema
54
+ properties = {}
55
+ required = []
56
+
57
+ if params_start and params_end:
58
+ for line in lines[params_start:params_end]:
59
+ if ':' in line:
60
+ param_info = line.split(':', 1)
61
+ param_name = param_info[0].strip()
62
+ param_desc = param_info[1].strip() if len(param_info) > 1 else ""
63
+
64
+ # Determine type from function annotations
65
+ func_params = func.__annotations__
66
+ param_type = "string" # default
67
+
68
+ if param_name in func_params:
69
+ annotation = func_params[param_name]
70
+ if annotation == int:
71
+ param_type = "integer"
72
+ elif annotation == bool:
73
+ param_type = "boolean"
74
+ elif annotation == float:
75
+ param_type = "number"
76
+ elif hasattr(annotation, '__origin__'):
77
+ # Handle List[str], etc.
78
+ if annotation.__origin__ == list:
79
+ param_type = "array"
80
+
81
+ properties[param_name] = {
82
+ "type": param_type,
83
+ "description": param_desc
84
+ }
85
+
86
+ # Check if parameter has no default value (is required)
87
+ import inspect
88
+ sig = inspect.signature(func)
89
+ if param_name in sig.parameters:
90
+ param = sig.parameters[param_name]
91
+ if param.default == inspect.Parameter.empty and param_name != 'self':
92
+ required.append(param_name)
93
+
94
+ # Create tool schema
95
+ tool = Tool(
96
+ name=func.__name__,
97
+ description=description,
98
+ inputSchema={
99
+ "type": "object",
100
+ "properties": properties,
101
+ "required": required
102
+ }
103
+ )
104
+
105
+ return tool
106
+
107
+
108
+ @app.list_tools()
109
+ async def list_tools() -> List[Tool]:
110
+ """List all available tools from enabled providers."""
111
+ tools = []
112
+
113
+ for provider_name, provider in providers.items():
114
+ provider_tools = provider.get_tools()
115
+ for tool_func in provider_tools:
116
+ tool_schema = create_tool_schema(tool_func, provider_name)
117
+ tools.append(tool_schema)
118
+
119
+ logger.info(f"Listed {len(tools)} tools from {len(providers)} providers")
120
+ return tools
121
+
122
+
123
+ @app.call_tool()
124
+ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
125
+ """Execute a tool from any enabled provider."""
126
+ logger.info(f"Calling tool: {name} with arguments: {arguments}")
127
+
128
+ # Find the tool in providers
129
+ for provider_name, provider in providers.items():
130
+ provider_tools = provider.get_tools()
131
+ for tool_func in provider_tools:
132
+ if tool_func.__name__ == name:
133
+ try:
134
+ # Call the tool function
135
+ result = await tool_func(**arguments)
136
+
137
+ # Return as TextContent
138
+ return [TextContent(
139
+ type="text",
140
+ text=str(result)
141
+ )]
142
+ except Exception as e:
143
+ logger.exception(f"Error calling tool {name}")
144
+ error_result = f'{{"error": "{str(e)}", "error_type": "{type(e).__name__}"}}'
145
+ return [TextContent(
146
+ type="text",
147
+ text=error_result
148
+ )]
149
+
150
+ # Tool not found
151
+ error_msg = f'{{"error": "Tool not found: {name}"}}'
152
+ return [TextContent(type="text", text=error_msg)]
153
+
154
+
155
+ async def main():
156
+ """Main entry point for the MCP server."""
157
+ global providers, http_client
158
+
159
+ # Load configuration
160
+ config = MCPConfig.from_env()
161
+
162
+ # Set logging level
163
+ logging.getLogger().setLevel(config.log_level)
164
+
165
+ logger.info("Starting Healthcare API MCP Server")
166
+ logger.info(f"Enabled providers: {config.enabled_providers}")
167
+
168
+ # Create shared HTTP client
169
+ http_client = create_http_client()
170
+
171
+ # Initialize providers
172
+ provider_classes = {
173
+ "openfda": OpenFDAProvider,
174
+ "clinical_guidelines": ClinicalGuidelinesProvider,
175
+ "cms_pricing": CMSPricingProvider
176
+ }
177
+
178
+ for provider_name in config.enabled_providers:
179
+ if provider_name not in provider_classes:
180
+ logger.warning(f"Unknown provider: {provider_name}")
181
+ continue
182
+
183
+ try:
184
+ provider_class = provider_classes[provider_name]
185
+
186
+ # Initialize provider with appropriate parameters
187
+ if provider_name == "openfda":
188
+ provider = provider_class(http_client, api_key=config.openfda_api_key)
189
+ else:
190
+ provider = provider_class(http_client)
191
+
192
+ await provider.initialize()
193
+ providers[provider_name] = provider
194
+ logger.info(f"Initialized provider: {provider_name}")
195
+ except Exception as e:
196
+ logger.error(f"Failed to initialize provider {provider_name}: {e}")
197
+
198
+ if not providers:
199
+ logger.error("No providers initialized. Exiting.")
200
+ return
201
+
202
+ logger.info(f"Successfully initialized {len(providers)} provider(s)")
203
+
204
+ # Run the server with stdio transport
205
+ try:
206
+ async with stdio_server() as (read_stream, write_stream):
207
+ logger.info("MCP server running on stdio")
208
+ await app.run(
209
+ read_stream,
210
+ write_stream,
211
+ app.create_initialization_options()
212
+ )
213
+ finally:
214
+ # Cleanup
215
+ logger.info("Shutting down...")
216
+ for provider in providers.values():
217
+ await provider.cleanup()
218
+ if http_client:
219
+ await http_client.aclose()
220
+
221
+
222
+ if __name__ == "__main__":
223
+ asyncio.run(main())
server_http.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HTTP Streaming MCP Server for Healthcare API MCP - HuggingFace Spaces deployment.
2
+
3
+ Uses HTTP streaming with chunked transfer encoding instead of SSE.
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import logging
9
+ from typing import AsyncGenerator
10
+
11
+ from fastapi import FastAPI, Request
12
+ from fastapi.responses import StreamingResponse
13
+ from starlette.middleware.cors import CORSMiddleware
14
+
15
+ from config import MCPConfig
16
+ from core import create_http_client
17
+ from providers.openfda_provider import OpenFDAProvider
18
+ from providers.clinical_guidelines_provider import ClinicalGuidelinesProvider
19
+ from providers.cms_pricing_provider import CMSPricingProvider
20
+
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Initialize FastAPI app
25
+ app = FastAPI(
26
+ title="Healthcare API MCP",
27
+ description="Model Context Protocol server for public healthcare data APIs",
28
+ version="1.0.0"
29
+ )
30
+
31
+ # Add CORS middleware
32
+ app.add_middleware(
33
+ CORSMiddleware,
34
+ allow_origins=["*"],
35
+ allow_credentials=True,
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+ # Global state
41
+ providers = {}
42
+ http_client = None
43
+
44
+
45
+ @app.on_event("startup")
46
+ async def startup():
47
+ """Initialize providers on startup."""
48
+ global http_client, providers
49
+
50
+ config = MCPConfig.from_env()
51
+ logger.info(f"Starting Healthcare API MCP with providers: {config.enabled_providers}")
52
+
53
+ http_client = create_http_client()
54
+
55
+ if "openfda" in config.enabled_providers:
56
+ providers["openfda"] = OpenFDAProvider(http_client, config.openfda_api_key)
57
+ await providers["openfda"].initialize()
58
+ logger.info("✅ OpenFDA provider initialized")
59
+
60
+ if "clinical_guidelines" in config.enabled_providers:
61
+ providers["clinical_guidelines"] = ClinicalGuidelinesProvider(http_client)
62
+ await providers["clinical_guidelines"].initialize()
63
+ logger.info("✅ Clinical Guidelines provider initialized")
64
+
65
+ if "cms_pricing" in config.enabled_providers:
66
+ providers["cms_pricing"] = CMSPricingProvider(http_client)
67
+ await providers["cms_pricing"].initialize()
68
+ logger.info("✅ CMS Pricing provider initialized")
69
+
70
+ logger.info(f"Healthcare API MCP ready with {len(providers)} provider(s)")
71
+
72
+
73
+ @app.on_event("shutdown")
74
+ async def shutdown():
75
+ """Cleanup on shutdown."""
76
+ global http_client, providers
77
+
78
+ logger.info("Shutting down Healthcare API MCP...")
79
+
80
+ for provider in providers.values():
81
+ await provider.cleanup()
82
+
83
+ if http_client:
84
+ await http_client.aclose()
85
+
86
+
87
+ @app.get("/")
88
+ async def root():
89
+ """Root endpoint with server info."""
90
+ return {
91
+ "name": "Healthcare API MCP",
92
+ "version": "1.0.0",
93
+ "protocol": "MCP over HTTP Streaming",
94
+ "providers": list(providers.keys()),
95
+ "endpoints": {
96
+ "tools": "/mcp/tools",
97
+ "call": "/mcp/call",
98
+ "stream": "/mcp/stream"
99
+ }
100
+ }
101
+
102
+
103
+ @app.get("/health")
104
+ async def health_check():
105
+ """Health check endpoint."""
106
+ return {
107
+ "status": "healthy",
108
+ "providers": {name: "active" for name in providers.keys()}
109
+ }
110
+
111
+
112
+ @app.get("/mcp/tools")
113
+ async def list_tools():
114
+ """List all available MCP tools."""
115
+ tools = []
116
+
117
+ for provider_name, provider in providers.items():
118
+ provider_tools = provider.get_tools()
119
+ for tool_func in provider_tools:
120
+ # Extract tool metadata
121
+ doc = tool_func.__doc__ or ""
122
+ description = doc.split("Args:")[0].strip() if "Args:" in doc else doc.strip()
123
+
124
+ # Parse parameters from docstring
125
+ params = {}
126
+ if "Args:" in doc:
127
+ args_section = doc.split("Args:")[1].split("Returns:")[0]
128
+ for line in args_section.split("\n"):
129
+ line = line.strip()
130
+ if ":" in line:
131
+ param_name = line.split(":")[0].strip()
132
+ param_desc = line.split(":", 1)[1].strip()
133
+ params[param_name] = param_desc
134
+
135
+ tools.append({
136
+ "name": tool_func.__name__,
137
+ "provider": provider_name,
138
+ "description": description,
139
+ "parameters": params
140
+ })
141
+
142
+ return {"tools": tools, "total": len(tools)}
143
+
144
+
145
+ @app.post("/mcp/call")
146
+ async def call_tool(request: Request):
147
+ """Call an MCP tool and return result."""
148
+ body = await request.json()
149
+ tool_name = body.get("tool")
150
+ arguments = body.get("arguments", {})
151
+
152
+ if not tool_name:
153
+ return {"error": "Missing 'tool' parameter"}
154
+
155
+ logger.info(f"Calling tool: {tool_name} with args: {arguments}")
156
+
157
+ # Find and execute tool
158
+ for provider_name, provider in providers.items():
159
+ provider_tools = provider.get_tools()
160
+ for tool_func in provider_tools:
161
+ if tool_func.__name__ == tool_name:
162
+ try:
163
+ result = await tool_func(**arguments)
164
+ return {
165
+ "success": True,
166
+ "tool": tool_name,
167
+ "result": result
168
+ }
169
+ except Exception as e:
170
+ logger.exception(f"Error calling tool {tool_name}")
171
+ return {
172
+ "success": False,
173
+ "tool": tool_name,
174
+ "error": str(e),
175
+ "error_type": type(e).__name__
176
+ }
177
+
178
+ return {
179
+ "success": False,
180
+ "tool": tool_name,
181
+ "error": f"Tool not found: {tool_name}"
182
+ }
183
+
184
+
185
+ async def stream_tool_result(tool_name: str, arguments: dict) -> AsyncGenerator[str, None]:
186
+ """Stream tool execution results as JSON chunks."""
187
+ # Send initial acknowledgment
188
+ yield json.dumps({
189
+ "type": "start",
190
+ "tool": tool_name,
191
+ "arguments": arguments
192
+ }) + "\n"
193
+
194
+ # Find and execute tool
195
+ tool_found = False
196
+ for provider_name, provider in providers.items():
197
+ provider_tools = provider.get_tools()
198
+ for tool_func in provider_tools:
199
+ if tool_func.__name__ == tool_name:
200
+ tool_found = True
201
+ try:
202
+ # Execute tool
203
+ result = await tool_func(**arguments)
204
+
205
+ # Stream result
206
+ yield json.dumps({
207
+ "type": "result",
208
+ "success": True,
209
+ "data": result
210
+ }) + "\n"
211
+
212
+ except Exception as e:
213
+ logger.exception(f"Error executing tool {tool_name}")
214
+ yield json.dumps({
215
+ "type": "error",
216
+ "success": False,
217
+ "error": str(e),
218
+ "error_type": type(e).__name__
219
+ }) + "\n"
220
+
221
+ break
222
+ if tool_found:
223
+ break
224
+
225
+ if not tool_found:
226
+ yield json.dumps({
227
+ "type": "error",
228
+ "success": False,
229
+ "error": f"Tool not found: {tool_name}"
230
+ }) + "\n"
231
+
232
+ # Send completion
233
+ yield json.dumps({"type": "complete"}) + "\n"
234
+
235
+
236
+ @app.post("/mcp/stream")
237
+ async def stream_tool_call(request: Request):
238
+ """Stream tool execution results using HTTP chunked transfer encoding."""
239
+ body = await request.json()
240
+ tool_name = body.get("tool")
241
+ arguments = body.get("arguments", {})
242
+
243
+ if not tool_name:
244
+ return {"error": "Missing 'tool' parameter"}
245
+
246
+ logger.info(f"Streaming tool: {tool_name} with args: {arguments}")
247
+
248
+ return StreamingResponse(
249
+ stream_tool_result(tool_name, arguments),
250
+ media_type="application/x-ndjson",
251
+ headers={
252
+ "Cache-Control": "no-cache",
253
+ "X-Accel-Buffering": "no",
254
+ }
255
+ )
256
+
257
+
258
+ if __name__ == "__main__":
259
+ import uvicorn
260
+ uvicorn.run(
261
+ app,
262
+ host="0.0.0.0",
263
+ port=7860,
264
+ log_level="info"
265
+ )