sources-mcp
by Eingin
README.md
# sources-mcp
Read-only MCP server that exposes the Eingin homelab's data sources to the self-hosted
[Cloudflare OS](https://github.com/cloudflare/cloudflare-os) instance at `cfos.eingin.dev`.
Three sources, each a group of tools:
| Source id | Data | Backing |
|---|---|---|
| `files` | the work laptop's documents | Syncthing → ZFS `vault/files`, bind-mounted read-only |
| `mail` | one Microsoft 365 mailbox | Microsoft Graph, app-only client credentials |
| `calls` | VoIPline call records | daily CSV report emailed to that mailbox, ingested into SQLite |
Everything is read-only. There is no write path in the code: no tool moves, deletes, sends
or uploads anything, and the file corpus is mounted `ReadOnlyPaths` under systemd so the
guarantee does not depend on the tool list being right.
## Why it looks like this
Cloudflare OS connects to external services through *gatekeepers*. Rather than writing three
gatekeepers in a fork of an early-access repo, this server implements the contract its
upstream `gatekeeper-mcp-portal` connector already speaks, so the cfos checkout stays
unpatched:
- one MCP Streamable-HTTP endpoint, `POST /mcp`, stateless, plain JSON responses;
- a tool literally named `portal_list_servers`, which is what marks an endpoint as a portal;
- every other tool named `<sourceId>_<tool>` — membership is decided by splitting at the
first underscore, so a source id may not contain one and `portal` is reserved;
- `annotations.readOnlyHint: true` on every tool, which is what makes a call an *observation*
rather than an approval-gated action;
- a static bearer token (`MCP_PORTAL_AUTH=token`), checked here and again by Caddy in front.
`src/portal.ts` documents the rest of the contract, with citations into the cfos source it
was derived from. The full design, including what was rejected and why, lives in
`homelab-iac/docs/plans/2026-08-13-cfos-data-sources.md`.
## Running
Node 24, run straight from TypeScript (native type stripping — no build step):
```sh
pnpm install
pnpm typecheck
pnpm test
node src/index.ts # the service
node src/ingest.ts # the daily VoIPline report ingester (systemd timer)
```
Configuration is environment-only; see `src/config.ts`. A source is registered only when its
configuration is present, so the service is deployable before the Entra app registration
exists or the first report has arrived — it simply serves fewer sources.
| Variable | Purpose |
|---|---|
| `SOURCES_API_TOKEN` | bearer token required on every request (≥32 chars) |
| `SOURCES_HOST` / `SOURCES_PORT` | listener; the deployment binds the container address |
| `SOURCES_FILES_ROOT` | corpus root, e.g. `/mnt/files`; unset disables the `files` source |
| `GRAPH_TENANT_ID` / `GRAPH_CLIENT_ID` / `GRAPH_CLIENT_SECRET` / `GRAPH_MAILBOX` | app-only Graph credential and the single mailbox it may read; all four or none |
| `SOURCES_CALLS_DB` | SQLite file for call records |
| `VOIPLINE_REPORT_FOLDER` | mail folder the CSV reports are filed into |
| `VOIPLINE_COLUMN_MAP` | optional JSON `{"<csv header>": "<field>"}` override for report columns |
Deployment (LXC container, systemd, nftables, Caddy) is `ansible/sources.yml` in the
`homelab-iac` repository.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing