Skip to main content
Glama
janstuemmel

notion-mcp

by janstuemmel
README.md
# Notion MCP

A self-hosted Streamable HTTP MCP server that mirrors a Notion workspace to
local Markdown files. MCP tools read the local mirror only; they never make a
Notion API request.

## Docker deployment

Prerequisites: Docker with the Compose plugin, and a Notion internal
integration token that has been connected to the pages and data sources to
mirror.

1. Create a private Docker network shared with MCP clients:

   ```sh
   docker network create notion-mcp-client
   ```

2. Copy `.env.example` to `.env` and set `NOTION_TOKEN`. Set
   `MCP_AUTH_TOKEN` to protect `/mcp` and `/status` with a bearer token.
3. Start the server:

   ```sh
   docker compose up -d --build
   ```

The Compose service has no published host ports. It is reachable only by
containers on `notion-mcp-client`, using `http://notion-mcp:3000`. Set
`MCP_NETWORK` before `docker compose up` to use a differently named existing
network.

The named `notion-mcp-data` volume persists the Markdown mirror and FTS index.
Restarting the service retains them, so unchanged pages are not downloaded
again.

The Linux x64 image measured 297 MiB in verification. The runtime stage
contains only `node:22-slim`, production dependencies (including the native
FTS binding), and compiled application output; compiler packages and
development dependencies remain in the discarded build stage.

## Client integration

Connect an MCP client container to the shared network. For example:

```sh
docker run --rm --network notion-mcp-client curlimages/curl \
  http://notion-mcp:3000/health
```

Configure a Streamable HTTP MCP client with URL
`http://notion-mcp:3000/mcp`. When `MCP_AUTH_TOKEN` is set, send
`Authorization: Bearer <MCP_AUTH_TOKEN>`. `/health` is intentionally public on
the private Docker network; `/mcp` and `/status` always pass through the auth
middleware.

## Environment

| Variable | Default | Description |
| --- | --- | --- |
| `NOTION_TOKEN` | required | Notion internal integration token. |
| `MCP_AUTH_TOKEN` | unset | Optional bearer token for `/mcp` and `/status`. |
| `EXPORT_DIR` | `/data/export` | Markdown mirror root. |
| `CACHE_DIR` | `/data/.cache` | SQLite FTS index location. |
| `PORT` | `3000` | HTTP listen port. |
| `BIND_HOST` | `0.0.0.0` | HTTP listen address. |
| `SYNC_INTERVAL_SECONDS` | `900` | Incremental sync interval. |
| `SYNC_OVERLAP_SECONDS` | `600` | Overlap behind the derived watermark. |
| `RECONCILE_EVERY_N_SYNCS` | `96` | Full-reconcile cadence. |
| `NOTION_RPS` | `2.5` | Notion request budget. |
| `NOTION_MAX_RETRIES` | `5` | Retry limit for retryable Notion failures. |
| `MIRROR_EXTERNAL_ASSETS` | `false` | Also mirror non-Notion asset URLs. |
| `SYNC_ON_STARTUP` | `true` | Run a sync when the service starts. |
| `LOG_LEVEL` | `info` | Pino log level. |

## Runbook

View service state and logs:

```sh
docker compose ps
docker compose logs -f notion-mcp
```

Check health from a sibling container:

```sh
docker run --rm --network notion-mcp-client curlimages/curl \
  http://notion-mcp:3000/health
```

Stop gracefully with `docker compose stop`. The process handles `SIGTERM`,
stops scheduling new syncs, closes HTTP connections, and Compose allows up to
30 seconds before forceful termination. Do not remove `notion-mcp-data` unless
you intend to discard the mirror and force a full sync on the next start.

To update, run `docker compose up -d --build`. To intentionally reset all
local mirror and search data, run `docker compose down -v`.

## Development

```sh
cp .env.example .env
pnpm install
pnpm build
pnpm start
```