Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
8a7b919
added sum to trend data
czajkub Sep 12, 2025
7795bf7
added device grouping to duckdb for test
czajkub Sep 12, 2025
a70831c
added device as well to query
czajkub Sep 12, 2025
74c5428
ch and duck device/interval grouping
czajkub Sep 12, 2025
954654c
docstring tweak
czajkub Sep 12, 2025
1afd7fe
docstring improving
czajkub Sep 12, 2025
0e42919
remove debug code
czajkub Sep 12, 2025
c3cbcb6
standardise errors and change trend docstrings
czajkub Sep 15, 2025
18e16d0
add localhost support for parquet
czajkub Sep 17, 2025
3beeb1f
Merge branch 'main' of https://github.com/czajkub/apple-health-mcp-se…
czajkub Sep 17, 2025
32e6dda
Merge branch 'main' of https://github.com/czajkub/apple-health-mcp-se…
czajkub Sep 17, 2025
fab22eb
remove debug from client
czajkub Sep 17, 2025
b619143
unterminated string
czajkub Sep 17, 2025
1c1678f
remove debug and add fileserver example
czajkub Sep 17, 2025
bd0bb50
Update README.md
czajkub Sep 17, 2025
664bbad
add fastapi to dev group
czajkub Sep 17, 2025
f0a90b1
Merge branch 'main' of https://github.com/czajkub/apple-health-mcp-se…
czajkub Sep 17, 2025
d6b01f8
Merge branch 'main' of https://github.com/czajkub/apple-health-mcp-se…
czajkub Sep 17, 2025
84b6428
Merge branch 'main' of https://github.com/czajkub/apple-health-mcp-se…
czajkub Sep 22, 2025
4204fc6
Merge branch 'tableschemas' of https://github.com/czajkub/apple-healt…
czajkub Sep 24, 2025
60e64bb
workouts and stats added as pq files
czajkub Sep 24, 2025
e73004b
concat check
czajkub Sep 24, 2025
4fa826d
asfas
czajkub Sep 24, 2025
d60551b
import fix
czajkub Sep 24, 2025
fabea70
is nto noene
czajkub Sep 24, 2025
554d9a5
tests
czajkub Sep 24, 2025
5b92857
order by sourcename + add unit tests for all queries from duckdb
czajkub Sep 24, 2025
5fa274e
linting i think + change textvalue case + all unit tests added
czajkub Sep 24, 2025
504576b
name fix
czajkub Sep 24, 2025
73c40ca
stupid linter
czajkub Sep 24, 2025
8f0d515
inspector workflow
czajkub Sep 25, 2025
08d5f88
add uv
czajkub Sep 25, 2025
dc9a6d2
add e2e tests with llm judge and test workflow
czajkub Sep 25, 2025
befeebb
workflow tweak
czajkub Sep 25, 2025
16b5250
hehehe
czajkub Sep 25, 2025
4d1889a
commit
czajkub Sep 25, 2025
b337cbb
ennvar
czajkub Sep 25, 2025
89e070b
added inspector test
czajkub Sep 25, 2025
c26fe15
node uv
czajkub Sep 25, 2025
817e671
dev deps
czajkub Sep 25, 2025
9e068f4
test file paht
czajkub Sep 25, 2025
6dcc5a8
mcp path
czajkub Sep 25, 2025
4d33a70
opik added
czajkub Sep 26, 2025
6500bd5
opik costam
czajkub Sep 26, 2025
31f94b1
comemgnae
czajkub Sep 26, 2025
4a3356f
curl mcp server
czajkub Sep 29, 2025
c6e7374
typo..
czajkub Sep 29, 2025
cf0f139
linting + cleaned up tests + added action
czajkub Sep 29, 2025
2eba7c9
using composite action
czajkub Sep 29, 2025
68b5e22
added shell and removed api keys
czajkub Sep 29, 2025
4457709
added mcp composite to opik tests
czajkub Sep 29, 2025
bb99d6c
debug
czajkub Sep 29, 2025
b9c1bb5
more debug
czajkub Sep 29, 2025
3face71
github guy fix
czajkub Sep 29, 2025
1128791
name fix typo
czajkub Sep 29, 2025
7ff2d57
workspace name fix
czajkub Sep 29, 2025
db279bc
cleanup & linting
czajkub Sep 29, 2025
55525e1
split readme
czajkub Sep 30, 2025
243955a
removing debug code & linting
czajkub Oct 1, 2025
fc45a4c
new parquet file example
czajkub Oct 1, 2025
4a801dd
changed test path
czajkub Oct 1, 2025
333a386
fixed trend data test
czajkub Oct 1, 2025
723d1cc
test debug for pipeline
czajkub Oct 1, 2025
f3fa00e
print test output in pipeline
czajkub Oct 1, 2025
7dca53a
readme merge conflict
czajkub Oct 1, 2025
003cb32
Merge branch 'the-momentum:main' into opiktests
czajkub Oct 1, 2025
c5a5924
Create tests.md
czajkub Oct 1, 2025
f8afe7e
rollback unstable changes
czajkub Oct 2, 2025
b3afd7c
Merge branch 'opiktests' of https://github.com/czajkub/apple-health-m…
czajkub Oct 2, 2025
674d75d
remove redundant tests
czajkub Oct 2, 2025
920a617
lint
czajkub Oct 2, 2025
2f38793
Update tests.md
czajkub Oct 2, 2025
a83535d
Update tests.md
czajkub Oct 2, 2025
4402014
test improvement
czajkub Oct 2, 2025
08c1286
Merge branch 'opiktests' of https://github.com/czajkub/apple-health-m…
czajkub Oct 2, 2025
a8de0be
add config for opik in .env
czajkub Oct 7, 2025
1116778
Merge branch 'the-momentum:main' into opiktests
czajkub Oct 7, 2025
47452da
Update README.md
czajkub Oct 7, 2025
b0674e7
Update tests.md
czajkub Oct 7, 2025
2b8185e
lint
czajkub Oct 7, 2025
883b51b
Merge branch 'opiktests' of https://github.com/czajkub/apple-health-m…
czajkub Oct 7, 2025
082bab2
Merge branch 'main' into opiktests
czajkub Oct 8, 2025
06e5f92
add test for workouts and tweak current tests
czajkub Oct 8, 2025
c1b72a8
changed example file
czajkub Oct 8, 2025
fee3925
changed workflow example
czajkub Oct 8, 2025
08b3c4e
changed pytest path
czajkub Oct 8, 2025
afe214d
linter
czajkub Oct 8, 2025
39e9eee
change composite action default value
czajkub Oct 8, 2025
2cacf9f
Update tests.md
czajkub Oct 8, 2025
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
34 changes: 34 additions & 0 deletions .github/workflows/mcp-composite-action/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: 'MCP server setup'
description: 'Sync uv dependencies and run MCP server'


inputs:
DUCKDB_FILENAME:
description: 'path to duckdb file'
required: false
default: 'tests/duckdb.example'

runs:
using: "composite"
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
shell: bash
- name: Install dependencies
run: uv sync --group dev
shell: bash
- name: Run fileserver
run: uv run --directory tests/ fileserver.py &
shell: bash
- name: Run mcp server
run: uv run fastmcp run -t http app/main.py &
env:
DUCKDB_FILENAME: ${{ inputs.DUCKDB_FILENAME }}
shell: bash
- name: Wait for mcp initialization
run: sleep 5
shell: bash
41 changes: 41 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: tests

on: [push]

env:
DUCKDB_FILENAME: tests/duckdb.example
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }}
OPIK_WORKSPACE: ${{ secrets.OPIK_WORKSPACE }}

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/workflows/mcp-composite-action
with:
DUCKDB_FILENAME: 'tests/duckdb.example'
- name: Run tests
run: uv run --directory tests/ pytest query_tests.py
inspector:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: ./.github/workflows/mcp-composite-action
with:
DUCKDB_FILENAME: 'tests/duckdb.example'
- name: Run inspector
run: npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list
opik:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: ./.github/workflows/mcp-composite-action
with:
DUCKDB_FILENAME: 'tests/duckdb.example'
- name: Run opik experiments
run: uv run tests/opik/tool_calls.py
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Want to try it out? **[🚀 Getting Started](docs/getting-started.md)**
- **[🔧 Configuration](docs/configuration.md)** - Environment variables and settings
- **[🛠️ MCP Tools](docs/mcp-tools.md)** - All available tools
- **[🗺️ Roadmap](docs/roadmap.md)** - Upcoming features and roadmap
- **[🧪 Testing](docs/tests.md)** - Instructions on tests and how to run them

**Need help?** Looking for guidance on use cases or implementation? Don't hesitate to ask your question in our [GitHub discussion forum](https://github.com/the-momentum/apple-health-mcp-server/discussions)! You'll also find interesting use cases, tips, and community insights there.

Expand Down
5 changes: 5 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class Settings(BaseSettings):
RAW_XML_PATH: str = "raw.xml"
XML_SAMPLE_SIZE: int = 1000

# Opik tests
OPENAI_API_KEY: str = ""
OPIK_WORKSPACE: str = ""
OPIK_API_KEY: str = ""

@field_validator("BACKEND_CORS_ORIGINS", mode="after")
@classmethod
def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:
Expand Down
2 changes: 1 addition & 1 deletion app/services/health/clickhouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def search_values_from_ch(
date_to: str | None = None,
) -> dict[str, Any]:
return ch.inquire(f"""
SELECT * FROM {ch.db_name}.{ch.table_name} WHERE textvalue = '{value}'
SELECT * FROM {ch.db_name}.{ch.table_name} WHERE textValue = '{value}'
{f"AND type = '{record_type}'" if record_type else ""}
{f"AND startDate >= '{date_from}'" if date_from else ""}
{f"AND startDate <= '{date_to}'" if date_to else ""}
Expand Down
4 changes: 2 additions & 2 deletions app/services/health/duckdb_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ def main() -> None:
pars = HealthRecordSearchParams(
limit=20,
record_type="HKWorkoutActivityTypeRunning",
date_from="2016-01-01T00:00:00+00:00",
date_to="2016-12-31T23:59:59+00:00",
min_workout_duration="45",
max_workout_duration="53",
)
logger.info(
f"records for search_health_records_from_duckdb: {search_health_records_from_duckdb(pars)}",
Expand Down
3 changes: 3 additions & 0 deletions config/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ ES_HOST="localhost"
DUCKDB_FILENAME="applehealth.duckdb"
CHUNK_SIZE="50000"
RAW_XML_PATH="raw.xml"
OPENAI_API_KEY="sk-proj-***"
OPIK_WORKSPACE="username"
OPIK_API_KEY="abcdef12345"
4 changes: 2 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ Follow these steps to set up Apple Health MCP Server in your environment.
- Run `make duckdb` to create a parquet file with your exported XML data
- If you want to connect to the file through http(s):
- The only thing you need to do is change the .env path, e.g. `localhost:8080/applehealth.parquet`
- If you want an example on how to host the files locally, run `uv run tests/fileserver.py`
- If you want an example on how to host the files locally, run `uv run tests/fileserver.py`


## Configuration Files

Expand Down
72 changes: 72 additions & 0 deletions docs/tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
[← Back to README](../README.md)

## Testing 🧪

There are 3 types of tests in this projects, all of which are included in the pipeline:

Every test is done on [pre-prepared mock apple health data](https://gist.github.com/czajkub/7ee7a01c35990f910f034f46dbf83b66):


## Unit tests 🔧:
- Testing the importing of XML data to .duckdb and database calls to DuckDB

## MCP Inspector tests 🔍:
- Uses the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) provided by Anthropic to test connection to the server hosted with streamable HTTP
- Mainly used in the pipeline, but can be run locally

## Opik tests 🤖:
- End-to-End tests using an agent created from [this](https://github.com/the-momentum/python-ai-kit) AI development kit
- Two types of tests:
- Checking whether the correct tool was called
- Judging the answer from an LLM by three metrics:
- Answer relevancy: whether the answer is relevant to the user's question 🎯
- Hallucination: whether the answer contains misleading or false information 🚫
- Levenshtein ratio: Heuristic checking the text structure similarity 📊

# How to run tests locally 💻:
- ### Unit tests 🔧:
```bash
pytest tests/query_tests.py
```

Before running the next tests, make sure you have the server up and running:
```bash
uv run fastmcp run -t http app/main.py
```

- ### Inspector tests 🔍:
```bash
npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport http --method tools/list
```

- ### Opik tests 🤖:
Make sure your `OPENAI_API_KEY`, `OPIK_WORKSPACE` and `OPIK_API_KEY` environmental variables are set
(Opik workspace refers to your profile name and not project name)
```bash
uv run tests/opik/tool_calls.py
```

### How to run Opik tests in pipeline:
- Create an account on Opik if you already haven't
- Copy your `OPIK_API_KEY` and `OPIK_WORKSPACE` to Github secrets


To add new tests, you can either do it in the code ([example from opik](https://www.comet.com/docs/opik/evaluation/manage_datasets)):
```python
import opik
# Get or create a dataset
client = opik.Opik()
dataset = client.get_or_create_dataset(name="My dataset")
# Add dataset items to it
dataset.insert([
{"user_question": "Hello, world!", "expected_output": {"assistant_answer": "Hello, world!"}},
{"user_question": "What is the capital of France?", "expected_output": {"assistant_answer": "Paris"}},
])
```

Or add it on the website:
<img width="1919" height="873" alt="image" src="https://github.com/user-attachments/assets/dc9f3807-40b4-4227-b4c2-5a1ea44396e7" />

When adding tool call questions, make sure the `input` and `tool_call` values are present, and when adding output checks make sure `input` and `expected_output` are set correctly.

[← Back to README](../README.md)
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ module-name = "app"
[dependency-groups]
dev = [
"fastapi>=0.116.2",
"opik>=1.8.56",
"pydantic-ai>=1.0.10",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pydantic-ai is for the agent used in opik tests

"pytest>=8.4.2",
"pytest-asyncio>=1.0.0",
"pytest-cov>=6.2.1",
Expand All @@ -50,7 +52,7 @@ exclude = ["./tests/", "./docs/", "./README.md"]
[tool.ruff]
line-length = 100
target-version = "py313"
extend-exclude = ["tests/", "./docs/", "./README.md"]
extend-exclude = ["./tests/", "./docs/", "./README.md"]

[tool.ruff.lint]
select = [
Expand Down
2 changes: 1 addition & 1 deletion scripts/clickhouse_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def create_table(self) -> None:
creationDate DateTime,
unit String,
value Float32,
textvalue String,
textValue String,
)
ENGINE = MergeTree
ORDER BY startDate
Expand Down
81 changes: 81 additions & 0 deletions tests/agent.py
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generated from (old) template

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import asyncio
import os

from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai import Agent
from pydantic_ai.tools import Tool
from pydantic_ai.mcp import MCPServerStreamableHTTP

from dotenv import load_dotenv
import opik

load_dotenv()

class AgentManager:
def __init__(self):
self.agent: Agent | None = None
self.mcp_client: MCPServerStreamableHTTP | None = None
self.tools: list[Tool] | None = None
self._initialized = False

async def initialize(self, model: str = "gpt-4o",
system_prompt: str | None = None):
if self._initialized:
return
try:
self.mcp_client = MCPServerStreamableHTTP("http://localhost:8000/mcp")
except Exception as e:
self.mcp_client = None
raise ConnectionError("Could not connect to MCP server") from e

if system_prompt is None:
system_prompt = "You are an AI assistant to help the user as best as you can. You can use the tools provided to you to help the user."

self.agent = self._create_agent(model, system_prompt)
self._initialized = True

def _create_agent(self, model: str, system_prompt: str) -> Agent:
model = OpenAIChatModel(model, provider=OpenAIProvider(api_key=os.getenv("openai_api_key")))
return Agent(
model=model,
deps_type=dict[str, str],
system_prompt=system_prompt,
toolsets=[self.mcp_client],
output_type=str,
)

@opik.track
async def handle_message(self, message: str) -> str:
if not self._initialized:
raise RuntimeError("Agent not initialized. Call initialize() first.")

async with self.agent:
result = await self.agent.run(message)
return result.output

def is_initialized(self) -> bool:
return self._initialized



agent_manager = AgentManager()


async def main():
await agent_manager.initialize()

try:
while True:
user_input = input("Enter your message: ")
if user_input == "exit":
break
print("User: ", user_input)
response = await agent_manager.handle_message(user_input)
print("Agent: ", response)
finally:
print("Closing agent")


if __name__ == "__main__":
asyncio.run(main())
Binary file added tests/duckdb.example
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generated from gist

Binary file not shown.
Loading