Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install dependencies
run: uv sync --group lint
run: uv sync --group code-quality
- name: Run linter
run: uv run ruff check
- name: Run formatter
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.1
rev: v0.12.12
hooks:
# Run the linter
- id: ruff
Expand All @@ -16,7 +16,7 @@ repos:
language: system

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
19 changes: 6 additions & 13 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
from pathlib import Path
from functools import lru_cache
from pathlib import Path

from pydantic import AnyHttpUrl, ValidationInfo, field_validator, SecretStr
from pydantic import AnyHttpUrl, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from app.utils.config_utils import EncryptedField, EnvironmentType
from app.utils.config_utils import EnvironmentType


class Settings(BaseSettings):
# FERNET_DECRYPTOR: FernetDecryptorField = Field("MASTER_KEY")

PROJECT_NAME: str = "MCP Server"
API_V1_STR: str = "/api/v1"
VERSION: str = "0.0.1"
Expand Down Expand Up @@ -39,17 +37,12 @@ class Settings(BaseSettings):
RAW_XML_PATH: str = "raw.xml"
XML_SAMPLE_SIZE: int = 1000

@field_validator("*", mode="after")
def _decryptor(cls, v, validation_info: ValidationInfo, *args, **kwargs):
if isinstance(v, EncryptedField):
return v.get_decrypted_value(validation_info.data["FERNET_DECRYPTOR"])
return v

@field_validator("BACKEND_CORS_ORIGINS", mode="after")
@classmethod
def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
if isinstance(v, (list, str)):
return v
raise ValueError(v)

Expand All @@ -61,7 +54,7 @@ def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:

@lru_cache
def get_settings() -> Settings:
return Settings()
return Settings() # type: ignore[call-arg


settings = get_settings()
3 changes: 1 addition & 2 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

from fastmcp import FastMCP

from app.mcp.v1.mcp import mcp_router
from app.config import settings

from app.mcp.v1.mcp import mcp_router

print("SETUP -> Setting up the app", file=sys.stderr)
mcp = FastMCP(settings.PROJECT_NAME)
Expand Down
2 changes: 1 addition & 1 deletion app/mcp/v1/mcp.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastmcp import FastMCP

from app.mcp.v1.tools import xml_reader, es_reader, ch_reader, duckdb_reader
from app.mcp.v1.tools import ch_reader, duckdb_reader, es_reader, xml_reader

mcp_router = FastMCP(name="Main MCP")

Expand Down
38 changes: 25 additions & 13 deletions app/mcp/v1/tools/ch_reader.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from typing import Any

from fastmcp import FastMCP

from app.schemas.record import RecordType, IntervalType, HealthRecordSearchParams
from app.schemas.record import HealthRecordSearchParams, IntervalType, RecordType
from app.services.health.clickhouse import (
get_health_summary_from_ch,
search_health_records_from_ch,
get_statistics_by_type_from_ch,
get_trend_data_from_ch,
search_health_records_from_ch,
)

ch_reader_router = FastMCP(name="CH Reader MCP")
Expand All @@ -16,7 +17,8 @@
def get_health_summary_ch() -> dict[str, Any]:
"""
Get a summary of Apple Health data from ClickHouse.
The function returns total record count, record type breakdown, and (optionally) a date range aggregation.
The function returns total record count, record type breakdown, and
(optionally) a date range aggregation.

Notes for LLM:
- IMPORTANT - Do not guess, autofill, or assume any missing data.
Expand All @@ -38,9 +40,11 @@ def search_health_records_ch(params: HealthRecordSearchParams) -> dict[str, Any]
- params: HealthRecordSearchParams object containing all search/filter parameters.

Notes for LLMs:
- This function should return a list of health record documents (dicts) matching the search criteria.
- This function should return a list of health record documents (dicts)
matching the search criteria.
- Each document in the list should represent a single health record as stored in ClickHouse.
- If an error occurs, the function should return a list with a single dict containing an 'error' key and the error message.
- If an error occurs, the function should return a list with a single dict
containing an 'error' key and the error message.
- Use this to retrieve structured health data for further analysis, filtering, or display.
- Example source_name: "Rob’s iPhone", "Polar Flow", "Sync Solver".
- Example date_from/date_to: "2020-01-01T00:00:00+00:00"
Expand All @@ -61,7 +65,8 @@ def get_statistics_by_type_ch(record_type: RecordType | str) -> dict[str, Any]:
Get comprehensive statistics for a specific health record type from ClickHouse.

Parameters:
- record_type: The type of health record to analyze. Use RecordType for most frequent types. Use str if that type is beyond RecordType scope.
- record_type: The type of health record to analyze. Use RecordType for
most frequent types. Use str if that type is beyond RecordType scope.

Returns:
- record_type: The analyzed record type
Expand All @@ -76,12 +81,17 @@ def get_statistics_by_type_ch(record_type: RecordType | str) -> dict[str, Any]:

Notes for LLMs:
- This function provides comprehensive statistical analysis for any health record type.
- The value_statistics object contains all basic statistics (count, min, max, avg, sum) for the 'value' field.
- The value_statistics object contains all basic statistics (count, min,
max, avg, sum) for the 'value' field.
- The sources breakdown shows which devices/apps contributed data for this record type.
- Example types: "HKQuantityTypeIdentifierStepCount", "HKQuantityTypeIdentifierBodyMassIndex", "HKQuantityTypeIdentifierHeartRate", etc.
- Use this function to understand the distribution, range, and trends of specific health metrics.
- The function is useful for health analysis, identifying outliers, and understanding data quality.
- date_range key for query is commented, since it contained hardcoded from date, but you can use it anyway if you replace startDate with your data.
- Example types: "HKQuantityTypeIdentifierStepCount",
"HKQuantityTypeIdentifierBodyMassIndex", "HKQuantityTypeIdentifierHeartRate", etc.
- Use this function to understand the distribution, range, and trends of
specific health metrics.
- The function is useful for health analysis, identifying outliers, and
understanding data quality.
- date_range key for query is commented, since it contained hardcoded from
date, but you can use it anyway if you replace startDate with your data.
- IMPORTANT - Do not guess, autofill, or assume any missing data.
- When asked for medical advice, ask the user whether he wants to use DuckDB, ClickHouse or
Elasticsearch.
Expand All @@ -100,7 +110,8 @@ def get_trend_data_ch(
date_to: str | None = None,
) -> dict[str, Any]:
"""
Get trend data for a specific health record type over time using ClickHouse date histogram aggregation.
Get trend data for a specific health record type over time using ClickHouse
date histogram aggregation.

Parameters:
- record_type: The type of health record to analyze (e.g., "HKQuantityTypeIdentifierStepCount")
Expand All @@ -120,7 +131,8 @@ def get_trend_data_ch(
Notes for LLMs:
- Use this to analyze trends, patterns, and seasonal variations in health data
- The function automatically handles date filtering if date_from/date_to are provided
- IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values.
- IMPORTANT - interval must be one of: "day", "week", "month", or "year".
Do not use other values.
- Do not guess, autofill, or assume any missing data.
- When asked for medical advice, ask the user whether he wants to use DuckDB, ClickHouse or
Elasticsearch.
Expand Down
38 changes: 25 additions & 13 deletions app/mcp/v1/tools/duckdb_reader.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from typing import Any

from fastmcp import FastMCP

from app.schemas.record import RecordType, IntervalType, HealthRecordSearchParams
from app.schemas.record import HealthRecordSearchParams, IntervalType, RecordType
from app.services.health.duckdb_queries import (
get_health_summary_from_duckdb,
search_health_records_from_duckdb,
get_statistics_by_type_from_duckdb,
get_trend_data_from_duckdb,
search_health_records_from_duckdb,
)

duckdb_reader_router = FastMCP(name="CH Reader MCP")
Expand All @@ -16,7 +17,8 @@
def get_health_summary_duckdb() -> list[dict[str, Any]]:
"""
Get a summary of Apple Health data from DuckDB.
The function returns total record count, record type breakdown, and (optionally) a date range aggregation.
The function returns total record count, record type breakdown, and
(optionally) a date range aggregation.

Notes for LLM:
- IMPORTANT - Do not guess, autofill, or assume any missing data.
Expand All @@ -38,9 +40,11 @@ def search_health_records_duckdb(params: HealthRecordSearchParams) -> list[dict[
- params: HealthRecordSearchParams object containing all search/filter parameters.

Notes for LLMs:
- This function should return a list of health record documents (dicts) matching the search criteria.
- This function should return a list of health record documents (dicts)
matching the search criteria.
- Each document in the list should represent a single health record as stored in ClickHouse.
- If an error occurs, the function should return a list with a single dict containing an 'error' key and the error message.
- If an error occurs, the function should return a list with a single dict
containing an 'error' key and the error message.
- Use this to retrieve structured health data for further analysis, filtering, or display.
- Example source_name: "Rob’s iPhone", "Polar Flow", "Sync Solver".
- Example date_from/date_to: "2020-01-01T00:00:00+00:00"
Expand All @@ -61,7 +65,8 @@ def get_statistics_by_type_duckdb(record_type: RecordType | str) -> list[dict[st
Get comprehensive statistics for a specific health record type from DuckDB.

Parameters:
- record_type: The type of health record to analyze. Use RecordType for most frequent types. Use str if that type is beyond RecordType scope.
- record_type: The type of health record to analyze. Use RecordType for
most frequent types. Use str if that type is beyond RecordType scope.

Returns:
- record_type: The analyzed record type
Expand All @@ -76,12 +81,17 @@ def get_statistics_by_type_duckdb(record_type: RecordType | str) -> list[dict[st

Notes for LLMs:
- This function provides comprehensive statistical analysis for any health record type.
- The value_statistics object contains all basic statistics (count, min, max, avg, sum) for the 'value' field.
- The value_statistics object contains all basic statistics (count, min,
max, avg, sum) for the 'value' field.
- The sources breakdown shows which devices/apps contributed data for this record type.
- Example types: "HKQuantityTypeIdentifierStepCount", "HKQuantityTypeIdentifierBodyMassIndex", "HKQuantityTypeIdentifierHeartRate", etc.
- Use this function to understand the distribution, range, and trends of specific health metrics.
- The function is useful for health analysis, identifying outliers, and understanding data quality.
- date_range key for query is commented, since it contained hardcoded from date, but you can use it anyway if you replace startDate with your data.
- Example types: "HKQuantityTypeIdentifierStepCount",
"HKQuantityTypeIdentifierBodyMassIndex", "HKQuantityTypeIdentifierHeartRate", etc.
- Use this function to understand the distribution, range, and trends of
specific health metrics.
- The function is useful for health analysis, identifying outliers, and
understanding data quality.
- date_range key for query is commented, since it contained hardcoded from
date, but you can use it anyway if you replace startDate with your data.
- IMPORTANT - Do not guess, autofill, or assume any missing data.
- When asked for medical advice, ask the user whether he wants to use DuckDB, ClickHouse or
Elasticsearch.
Expand All @@ -100,7 +110,8 @@ def get_trend_data_duckdb(
date_to: str | None = None,
) -> list[dict[str, Any]]:
"""
Get trend data for a specific health record type over time using DuckDB date histogram aggregation.
Get trend data for a specific health record type over time using DuckDB
date histogram aggregation.

Parameters:
- record_type: The type of health record to analyze (e.g., "HKQuantityTypeIdentifierStepCount")
Expand All @@ -120,7 +131,8 @@ def get_trend_data_duckdb(
Notes for LLMs:
- Use this to analyze trends, patterns, and seasonal variations in health data
- The function automatically handles date filtering if date_from/date_to are provided
- IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values.
- IMPORTANT - interval must be one of: "day", "week", "month", or "year".
Do not use other values.
- Do not guess, autofill, or assume any missing data.
- When asked for medical advice, ask the user whether he wants to use DuckDB, ClickHouse or
Elasticsearch.
Expand Down
38 changes: 25 additions & 13 deletions app/mcp/v1/tools/es_reader.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from typing import Any

from fastmcp import FastMCP

from app.schemas.record import RecordType, IntervalType, HealthRecordSearchParams
from app.schemas.record import HealthRecordSearchParams, IntervalType, RecordType
from app.services.health.elasticsearch import (
get_health_summary_from_es,
search_health_records_logic,
get_statistics_by_type_logic,
get_trend_data_logic,
search_health_records_logic,
)

es_reader_router = FastMCP(name="ES Reader MCP")
Expand All @@ -16,7 +17,8 @@
def get_health_summary_es() -> dict[str, Any]:
"""
Get a summary of Apple Health data from Elasticsearch.
The function returns total record count, record type breakdown, and (optionally) a date range aggregation.
The function returns total record count, record type breakdown, and
(optionally) a date range aggregation.

Notes for LLM:
- IMPORTANT - Do not guess, auto-fill, or assume any missing data.
Expand All @@ -38,9 +40,11 @@ def search_health_records_es(params: HealthRecordSearchParams) -> list[dict[str,
- params: HealthRecordSearchParams object containing all search/filter parameters.

Notes for LLMs:
- This function should return a list of health record documents (dicts) matching the search criteria.
- This function should return a list of health record documents (dicts)
matching the search criteria.
- Each document in the list should represent a single health record as stored in Elasticsearch.
- If an error occurs, the function should return a list with a single dict containing an 'error' key and the error message.
- If an error occurs, the function should return a list with a single dict
containing an 'error' key and the error message.
- Use this to retrieve structured health data for further analysis, filtering, or display.
- Example source_name: "Rob’s iPhone", "Polar Flow", "Sync Solver".
- Example date_from/date_to: "2020-01-01T00:00:00+00:00"
Expand All @@ -61,7 +65,8 @@ def get_statistics_by_type_es(record_type: RecordType | str) -> dict[str, Any]:
Get comprehensive statistics for a specific health record type from Elasticsearch.

Parameters:
- record_type: The type of health record to analyze. Use RecordType for most frequent types. Use str if that type is beyond RecordType scope.
- record_type: The type of health record to analyze. Use RecordType for
most frequent types. Use str if that type is beyond RecordType scope.

Returns:
- record_type: The analyzed record type
Expand All @@ -76,12 +81,17 @@ def get_statistics_by_type_es(record_type: RecordType | str) -> dict[str, Any]:

Notes for LLMs:
- This function provides comprehensive statistical analysis for any health record type.
- The value_statistics object contains all basic statistics (count, min, max, avg, sum) for the 'value' field.
- The value_statistics object contains all basic statistics (count, min,
max, avg, sum) for the 'value' field.
- The sources breakdown shows which devices/apps contributed data for this record type.
- Example types: "HKQuantityTypeIdentifierStepCount", "HKQuantityTypeIdentifierBodyMassIndex", "HKQuantityTypeIdentifierHeartRate", etc.
- Use this function to understand the distribution, range, and trends of specific health metrics.
- The function is useful for health analysis, identifying outliers, and understanding data quality.
- date_range key for query is commented, since it contained hardcoded from date, but you can use it anyway if you replace startDate with your data.
- Example types: "HKQuantityTypeIdentifierStepCount",
"HKQuantityTypeIdentifierBodyMassIndex", "HKQuantityTypeIdentifierHeartRate", etc.
- Use this function to understand the distribution, range, and trends of
specific health metrics.
- The function is useful for health analysis, identifying outliers, and
understanding data quality.
- date_range key for query is commented, since it contained hardcoded from
date, but you can use it anyway if you replace startDate with your data.
- IMPORTANT - Do not guess, auto-fill, or assume any missing data.
- When asked for medical advice, ask the user whether he wants to use DuckDB, ClickHouse or
Elasticsearch.
Expand All @@ -100,7 +110,8 @@ def get_trend_data_es(
date_to: str | None = None,
) -> dict[str, Any]:
"""
Get trend data for a specific health record type over time using Elasticsearch date histogram aggregation.
Get trend data for a specific health record type over time using
Elasticsearch date histogram aggregation.

Parameters:
- record_type: The type of health record to analyze (e.g., "HKQuantityTypeIdentifierStepCount")
Expand All @@ -120,7 +131,8 @@ def get_trend_data_es(
Notes for LLMs:
- Use this to analyze trends, patterns, and seasonal variations in health data
- The function automatically handles date filtering if date_from/date_to are provided
- IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values.
- IMPORTANT - interval must be one of: "day", "week", "month", or "year".
Do not use other values.
- Do not guess, auto-fill, or assume any missing data.
- When asked for medical advice, ask the user whether he wants to use DuckDB, ClickHouse or
Elasticsearch.
Expand Down
Loading