android-timeline-mcp
Read-only MCP server exposing Android phone timeline data, including events, hourly/daily features, data coverage, and gaps, so Claude can answer questions about observed phone activity.
README
android-timeline-home-assistant
Status: experimental. The server side is exercised end to end in CI against the real collector, but no physical Android phone has ever fed it. See Current limitations.
A Home Assistant app (formerly "add-on") that receives raw timestamped
events from the
android-timeline-termux
collector, stores them immutably, derives versioned hourly and daily
features, measures how much of each day was actually observed, publishes a
handful of summary entities, and exposes a read-only MCP server so
Claude can ask questions about the data.
What it does
- Authenticated ingestion. Device-specific bearer tokens, stored only as
keyed hashes. Idempotent on
event_id, so a replay is always a no-op. - Immutable raw events. Features are derived and versioned; recomputing them never rewrites history.
- Explicit missingness. Every hour is a row, including the empty ones. A flat line and a dead collector are different things and are reported differently.
- Read-only MCP. No SQL, no shell, no filesystem, no mutation, no phone control. Bounded ranges, pagination, and redaction that cannot be turned off.
- Minimal privileges. Ingress only, no published port, no Supervisor role, no Home Assistant directory mounted.
Architecture
flowchart TD
phone["Android phone<br/><i>android-timeline-termux</i>"]
subgraph app["Home Assistant app (this repository)"]
direction TB
api["FastAPI ingestion<br/>POST /api/v1/events/batch"]
auth["Device token auth<br/>+ rate limiting"]
store[("SQLite event store<br/>append-only")]
features["Feature pipeline<br/>hourly + daily, versioned"]
cov["Coverage and gap detection"]
tl["Timeline API<br/>GET /api/v1/timeline/yesterday"]
mcp["Read-only MCP server<br/>/mcp"]
ent["Summary entity publisher"]
api --> auth --> store
store --> features --> cov
cov --> tl
cov --> mcp
features --> ent
end
ha["Home Assistant Core<br/>sensor.* / binary_sensor.*"]
claude["Claude / MCP client"]
phone -->|"HTTPS, Bearer token,<br/>X-Batch-ID"| api
api -->|"per-event acknowledgement"| phone
ent -->|"Supervisor API proxy"| ha
mcp --> claude
Installation
-
In Home Assistant: Settings -> Apps -> App store -> ⋮ -> Repositories
-
Add
https://github.com/resace3/android-timeline-home-assistant -
Install Android Timeline from the store, then Start it.
-
Open the app's web UI (ingress) to reach the diagnostic page.
-
Enroll your phone:
curl -X POST http://<app>/api/v1/admin/devices \ -H 'Content-Type: application/json' \ -d '{"device_id": "device-my-pixel-001"}'The response contains the device token once. Copy it into the collector's
~/.config/android-timeline/token. Only a keyed hash is stored here; there is no way to recover it later, only to rotate.
Full details, including running behind ingress and the optional custom integration: docs/installation.md.
Configuration
| Option | Default | Notes |
|---|---|---|
timezone |
UTC |
Where a "day" starts. Events are always stored in UTC. |
features_enabled |
true |
Turning it off stops the pipeline; nothing is deleted. |
publish_entities |
true |
Summary entities only, never one per event. |
mcp_enabled |
true |
Read-only MCP endpoint, ingress-protected. |
trust_ingress_admin |
true |
Safe because no port is published. |
admin_token |
(empty) | Only needed outside ingress. |
retention_enabled |
false |
Raw events are kept forever by default. |
API
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/v1/health |
none | liveness; no user data |
POST /api/v1/events/batch |
device token | ingestion |
GET /api/v1/devices/{id}/status |
device token (own device) | queue and sync state |
GET /api/v1/timeline/yesterday |
admin | the day timeline |
GET /api/v1/timeline/{date} |
admin | a specific local date |
GET /api/v1/features/hourly |
admin | stored hourly features |
GET /api/v1/coverage |
admin | coverage and gaps |
POST /api/v1/admin/devices |
admin | enroll, returns a token once |
POST /api/v1/admin/devices/{id}/rotate |
admin | rotate, revoking the old token |
DELETE /api/v1/admin/tokens/{id} |
admin | revoke |
MCP
Twelve read-only tools: list_devices, list_phone_sources,
get_phone_latest, query_phone_events, get_day_timeline,
get_hourly_features, get_daily_features, get_data_coverage,
find_data_gaps, get_collector_status, export_phone_window and
describe_server.
Guaranteed absent: arbitrary SQL, shell execution, filesystem access, mutation of any kind, and phone control. Date ranges are capped at 31 days, pages at 1000 rows, and precise coordinates, message bodies and contact names are stripped with no parameter to re-enable them.
Client configuration (placeholders only): docs/mcp.md.
Testing
| Layer | Status | What it proves |
|---|---|---|
| Unit | required | database, auth, ingestion, features, coverage, timeline |
| API + MCP | required | the HTTP surface and the MCP protocol, in-process |
| Contract | required | Pydantic models and published JSON Schemas agree |
| E2E (in-process) | required | synthetic day -> ingest -> features -> timeline -> MCP, across a restart |
| Container (Layer A) | required | the real image: build, boot, drive, restart |
| Mock Supervisor (Layer B) | required | the one Supervisor endpoint this app calls |
| App image build (Layer D) | required | current official HA builder actions, amd64 + aarch64 |
| Cross-repository E2E | required | the real collector uploading to the real image |
| Home Assistant Core (Layer C) | non-blocking | the custom integration loads in a real HA container |
| Supervised HA (Layer E) | not attempted | see below |
The cross-repository workflow installs the actual collector from
android-timeline-termux at a pinned revision, runs its own outbox and
uploader against a container built from this repository, then asserts on
storage, idempotency, features, coverage and MCP. Details:
docs/testing.md.
Current limitations
Stated plainly:
- No physical phone has ever fed this. Every event CI has ever ingested is synthetic.
- Supervisor is not tested in CI. A real Supervisor needs privileged Docker-in-Docker or a full OS image; a flaky green result would be worse than an honest gap. Layer B tests the one endpoint this app actually calls, and nothing more.
- Ingress trust is a design assumption. Treating an ingress request as
an authenticated admin is only safe because no port is published. If you
publish one, set
admin_tokenand turntrust_ingress_adminoff. - SQLite, single writer. Fine for a personal deployment; not a fleet.
- Features are approximations.
charging_minutesandwifi_connected_minutesare inferred from periodic samples, so a state change between samples is invisible. Every value ships with a coverage proportion for exactly this reason. - The timeline has no polished UI. The
/page is a diagnostic table.
Privacy considerations
- Raw events are stored as the phone sent them; the phone is where redaction happens, and it defaults to metadata only.
- The MCP server redacts precise coordinates, message bodies and contact names again on the way out, with no override.
- Home Assistant entities carry aggregates only, so the recorder database never accumulates behavioural detail.
- No fixture, test or CI artifact contains real personal data, and CI fails if a credential-shaped string appears anywhere in history.
Relationship to the other repository
android-timeline-termux |
runs on the phone: collection, the outbox, the upload client |
| This repository | runs in Home Assistant: ingestion, storage, features, coverage, MCP |
This repository is the source of truth for the protocol schemas in
schemas/. The collector vendors a byte-identical copy; the
cross-repository workflow diffs them directly and fails on any difference.
Both speak protocol_version 1. Compatibility policy:
docs/data-model.md.
Development
python -m pip install -e ".[dev]"
ruff format . && ruff check . && mypy
pytest
Run the whole pipeline locally without Home Assistant:
docker compose -f test-harness/docker-compose.yml up --build
Roadmap
- [ ] Ingest from a real phone (blocking for 1.0)
- [ ] QR-code device enrollment
- [ ] A real timeline UI
- [ ] Optional Postgres backend for larger deployments
- [ ] Intervention annotations feeding the causal-inference use case
- [ ] Feature definitions v2 with dwell-time and place transitions
License
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.