> ## Documentation Index
> Fetch the complete documentation index at: https://fracta.quasarops.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Setup

> Wire Notion and Readwise to the fracta gateway via native OAuth, stand up the concept extractors, provision the three Notion databases, and verify the stack — about 20 minutes.

Setup gets you from a fresh fracta install to a registry with five MCP servers — `notion`, `readwise`, and the three concept extractors — all green, plus three Notion databases ready to receive published pages. About 20 minutes of work: two OAuth pop-ups, an agent prompt that creates the three Notion databases for you (or a manual click-through if you'd rather), and a `kubectl rollout restart` to pick up the rotated tokens.

The pipeline run itself lives on [First Run](/docs/patterns/reading-garden/first-run); this page is purely the wiring.

<Note>
  **Strategy gateway access.** This pattern's strategies (`highlight-distill`, `notion-publish`) call MCP tools inline via `ctx.mcp.call_tool(...)` — so the strategy runner must be able to dial the gateway during a run. Since v0.5.2 the scaffold ships with `strategy.gateway_access: true` in the gateway ConfigMap and the runner sidecar wired with `--gateway-url`. If you upgraded from an older deploy, confirm the flag is present in your `fracta-gateway` ConfigMap before running the pipeline — the runner refuses to start (`requires.mcp: true but ctx.mcp is None`) without it.
</Note>

## Prerequisites

<Steps>
  <Step title="Fracta installed and a runtime configured">
    Follow [Installation](/docs/getting-started/installation) and [Your First Agent](/docs/getting-started/first-agent). Confirm `fracta spawn --task hello --contract "say hi"` returns cleanly before continuing.
  </Step>

  <Step title="A Readwise (or Reader) account with at least one synced highlight">
    Free accounts work. The pipeline reads through the hosted [Readwise MCP](https://readwise.io/mcp) — no API key is needed; fracta handles the OAuth grant.
  </Step>

  <Step title="A Notion workspace where you can create an integration and a database">
    You need a workspace where you (or your OAuth grant) can create new databases. Personal workspaces work; in shared workspaces, the workspace admin's permissions during the OAuth consent determine what the integration can do.
  </Step>

  <Step title="A Kubernetes cluster reachable from your laptop">
    The reading-garden pattern targets the [Kubernetes deployment](/docs/guides/deployment/kubernetes) (kind is the recommended default). The fracta gateway runs in-cluster and consumes the OAuth tokens as mounted Secrets. Confirm `kubectl cluster-info` returns your kind/Docker Desktop/k3d cluster.
  </Step>

  <Step title="Docker for the concept extractors">
    The three extractor MCPs (`concept-keybert`, `concept-gliner`, `concept-spacy`) ship as containers only — `local_process: not_supported` in the catalog. The footprint matters; see the box below before you commit.
  </Step>
</Steps>

<Warning>
  **Extractor footprint.** The three extractor containers together pull roughly **5.3 GB of images** and hold **1.5–2 GB of RAM resident** once their models are loaded (`concept-gliner` alone keeps \~1.4 GB of DeBERTa-v3 weights warm). Plan for a laptop or homelab-class host. A Raspberry Pi 4 will swap; a small VPS without 4 GB+ of RAM headroom will OOM under load.
</Warning>

## Step 1 — Wire the hosted MCPs through fracta's native OAuth

Both Notion and Readwise expose hosted MCP endpoints that require OAuth with PKCE. Fracta drives the OAuth dance itself: the `fracta config mcp auth login <server>` command opens your browser, completes the consent flow, and stores the resulting tokens in the OS keyring (macOS Keychain under the `fracta.oauth` service, libsecret on Linux). You then export those tokens as a Kubernetes Secret and apply it to the cluster — the gateway picks them up from a mounted file at boot.

No Node, no `npx`, no `mcp-remote` stdio bridge, no plaintext token cache.

### 1.1 — Register the servers in the gateway config

The gateway needs to know which servers exist before you can log into them. Both Notion and Readwise are already in the fracta catalog (`mcp-servers/{notion,readwise}/server.yaml`), and `fracta init --scaffold k8s` writes a starter gateway ConfigMap that includes both. If your scaffold predates spec-41, add them under `mcp_servers.servers:` in `deployment/k8s/manifests/fracta-gateway.yaml`'s ConfigMap data:

```yaml theme={null}
mcp_servers:
  servers:
    notion:
      remote:
        url: https://mcp.notion.com/mcp
        transport: streamable-http
        auth:
          type: oauth
          pkce: true
          token_file: /etc/fracta/oauth/notion/token.json
          client_registration_file: /etc/fracta/oauth/notion/client-registration.json

    readwise:
      remote:
        url: https://mcp2.readwise.io/mcp
        transport: streamable-http
        auth:
          type: oauth
          pkce: true
          token_file: /etc/fracta/oauth/readwise/token.json
          client_registration_file: /etc/fracta/oauth/readwise/client-registration.json
```

Then add matching `volumeMounts` on the gateway container and `volumes` referencing the Secrets (the Secrets themselves don't exist yet — `optional: true` lets the pod start anyway):

```yaml theme={null}
# spec.template.spec.containers[0].volumeMounts:
- name: notion-oauth
  mountPath: /etc/fracta/oauth/notion
  readOnly: true
- name: readwise-oauth
  mountPath: /etc/fracta/oauth/readwise
  readOnly: true

# spec.template.spec.volumes:
- name: notion-oauth
  secret:
    secretName: fracta-mcp-notion
    optional: true
- name: readwise-oauth
  secret:
    secretName: fracta-mcp-readwise
    optional: true
```

Apply the manifest now (`kubectl apply -k deployment/k8s/manifests/`). The gateway will boot with `auth_required` on both servers — that's expected.

### 1.2 — Run the OAuth flow locally

The `fracta config mcp auth login <server>` command reads its server definition from a **local** YAML file passed via `--config` — it does **not** introspect the in-cluster gateway ConfigMap you edited in step 1.1. You'll create two **throwaway "login config" files** in your fracta project root, one per server. They exist only to tell the CLI which OAuth endpoint to dance with; they hold no secrets and can be deleted (or kept for re-auth, your call) the moment step 1.4 succeeds.

Copy-paste these two files verbatim into your project root:

```yaml theme={null}
# notion-login.yaml — throwaway helper, only used by `fracta config mcp auth {login,export} notion --config ...`
mcp_servers:
  servers:
    notion:
      remote:
        url: https://mcp.notion.com/mcp
        transport: streamable-http
        auth:
          type: oauth
          pkce: true
```

```yaml theme={null}
# readwise-login.yaml — throwaway helper, only used by `fracta config mcp auth {login,export} readwise --config ...`
mcp_servers:
  servers:
    readwise:
      remote:
        url: https://mcp2.readwise.io/mcp
        transport: streamable-http
        auth:
          type: oauth
          pkce: true
```

These describe only the OAuth endpoint shape — no `token_file` / `client_registration_file` paths, because the CLI is about to write fresh tokens to your OS keyring, not read them from disk. The "real" server config — with the in-cluster token paths — already lives in the gateway ConfigMap you edited in step 1.1.

Then, from your fracta project root:

```bash theme={null}
fracta config mcp auth login notion   --config ./notion-login.yaml
fracta config mcp auth login readwise --config ./readwise-login.yaml
```

Each command opens your default browser at the provider's consent page. Approve, and the callback returns to a local port. The tokens (access + refresh, plus the dynamically-registered OAuth client) land in your OS keyring under service `fracta.oauth`, accounts `<server>:token` and `<server>:client`.

<Note>
  **Why a separate login config?** The CLI's auth subcommand is intentionally decoupled from the in-cluster gateway — you can run logins on a laptop that has never connected to your cluster, or rotate tokens for a server that isn't deployed yet. If you omit `--config`, the CLI looks at your default `fracta.yaml`, which in the kubernetes scaffold only describes the thin-client control-plane connection and won't contain the OAuth servers. The error is `server "<name>" not found in config`.
</Note>

<Note>
  **Static Notion integration tokens do not work for the hosted MCP.** The internal-integration `secret_...` token issued by Notion's "My integrations" page authenticates the REST API, not `mcp.notion.com/mcp`. The hosted MCP only accepts OAuth — which is exactly what `fracta config mcp auth login notion` performs. If you have an existing integration token wired into other tools, leave it where it is; this stack uses a separate OAuth grant.
</Note>

### 1.3 — Export the tokens as Kubernetes Secrets

```bash theme={null}
fracta config mcp auth export notion   --format k8s-secret --config ./notion-login.yaml   2>/dev/null > notion-oauth-secret.yaml
fracta config mcp auth export readwise --format k8s-secret --config ./readwise-login.yaml 2>/dev/null > readwise-oauth-secret.yaml
```

Each command emits an `Opaque` Secret named `fracta-mcp-<server>` with two keys: `token.json` (access + refresh tokens) and `client-registration.json` (the dynamically-registered client ID). The `--config` flag points back at the same login YAML you used in step 1.2 — export and login share a single source of truth for which server they're operating on.

The `2>/dev/null` swallows a non-fatal warning the thin-client config validator emits about `runtime.staging_dir` (the login YAML omits it, since OAuth login doesn't need staging). Without the redirect, that line lands inside the YAML you're writing to disk and corrupts the Secret. Confirm the first line of each output file is `apiVersion: v1` before applying.

<Warning>
  **The exported Secret YAMLs are transit artifacts, not config.** `notion-oauth-secret.yaml` and `readwise-oauth-secret.yaml` carry long-lived refresh tokens in plaintext — anyone with one of those files can call the hosted MCP as you until you revoke the grant.

  **Preferred handling**: once `kubectl apply` has loaded them into the cluster (step 1.4), stash a copy in your password manager for re-auth or DR, then `rm` them from disk. The keyring entries on your laptop and the in-cluster Secret are the durable copies; the YAMLs are just the wire format that bridges them.

  **At minimum**: add `*-oauth-secret.yaml` to your `.gitignore` **before** the first `git add`. The `notion-login.yaml` / `readwise-login.yaml` helpers from step 1.2 are harmless to commit (they contain no secrets), but you can gitignore them too if you'd rather not have throwaway files in version control.
</Warning>

### 1.4 — Apply the Secrets and pick them up

```bash theme={null}
kubectl apply -f notion-oauth-secret.yaml -n fracta
kubectl apply -f readwise-oauth-secret.yaml -n fracta
kubectl rollout restart deploy/fracta-gateway -n fracta
kubectl rollout status  deploy/fracta-gateway -n fracta
```

After the rollout, tail the gateway logs:

```bash theme={null}
kubectl logs -n fracta deploy/fracta-gateway --tail=60 | grep -iE "MCP client connected|server=(notion|readwise)"
```

You should see two `MCP client connected` lines — one per server — each reporting a tool count (18 for `notion`, \~22 for `readwise`).

<Note>
  **Re-authentication.** The OAuth refresh tokens are long-lived (Readwise's currently outlive the access token by months). When you do need to rotate — provider revoked, expired, or a new scope — repeat steps 1.2–1.4, reusing the same `<server>-login.yaml` files with `--config`. There's no separate "logout" command needed on the cluster side; `kubectl apply` overwrites the existing Secret and the gateway rollout picks up the new file. Locally, `fracta config mcp auth logout <server>` clears the keyring entries before a fresh login.
</Note>

## Step 2 — Provision the three Notion databases

Since v0.5.2 the Reading Garden pipeline publishes into a **three-database mirror** — Sources, Highlights, and Concepts — linked by Notion RELATION columns. Each Notion page comes back from the API with its `data_source_id`, which is what the strategy params take. Create all three now and capture their IDs.

You have two paths: let an agent create the databases for you via the Notion MCP, or click through three create-database flows in the Notion UI. The agent path is faster and gets the schemas exactly right.

<Tabs>
  <Tab title="Agent-driven (recommended)">
    The OAuth grant you completed in Step 1 gives the fracta gateway the same Notion permissions your user has — including creating new databases. Drive that from any agent CLI with a Notion MCP connection (Claude Code, Codex, OpenCode), or spawn a fracta worker that talks back to your gateway.

    **Via a fracta worker** (uses the gateway's already-configured Notion connection):

    ```bash theme={null}
    fracta spawn --task provision-reading-garden \
      --contract "$(cat <<'PROMPT'
    Create three Notion databases in my workspace for the fracta Reading Garden pipeline. Use the notion-create-database tool. After each one is created, call notion-fetch on the returned page id and report the data_source_id.

    1) Title: 'Reading Garden — Sources'. Properties:
       - Name (title)
       - readwise_book_id (rich_text)
       - author (rich_text)
       - category (rich_text)
       - source_kind (rich_text)
       - source_url (url)
       - cover_url (url)
       - document_note (rich_text)

    2) Title: 'Reading Garden — Highlights'. Properties:
       - Name (title)
       - readwise_highlight_id (rich_text)
       - note (rich_text)
       - highlighted_at (date)
       - source (relation -> the 'Reading Garden — Sources' database from step 1)

    3) Title: 'Reading Garden — Concepts'. Properties:
       - Name (title)
       - concept_name (rich_text)
       - epistemic_status (select: seedling, budding, evergreen)
       - confidence (number)
       - mention_count (number)
       - last_updated (date)
       - highlights (relation -> the 'Reading Garden — Highlights' database from step 2)

    Report back the three data_source_id values, labelled sources / highlights / concepts.
    PROMPT
    )"

    fracta peek provision-reading-garden
    ```

    **Via your local agent CLI** (Claude Code / Codex / OpenCode with the Notion MCP wired): paste the same prompt block (the part between the `PROMPT` markers) into your agent session. The agent's Notion MCP must be the hosted `mcp.notion.com/mcp` server (the same one fracta uses), authenticated to the same workspace you ran OAuth against in Step 1.

    The data\_source\_id values feed straight into the [First Run](/docs/patterns/reading-garden/first-run) prompt as `notion_sources_database_id`, `notion_highlights_database_id`, and `notion_concepts_database_id`.
  </Tab>

  <Tab title="Manual (Notion UI)">
    Create three databases by hand, then read back their data\_source\_ids via a `notion-search` + `notion-fetch` call.

    <Steps>
      <Step title="Create the Sources database">
        In Notion, create a new database titled "Reading Garden — Sources" with these properties (default `Name` plus):

        * `readwise_book_id` (text)
        * `author` (text)
        * `category` (text)
        * `source_kind` (text)
        * `source_url` (URL)
        * `cover_url` (URL)
        * `document_note` (text)
      </Step>

      <Step title="Create the Highlights database">
        Title: "Reading Garden — Highlights". Properties:

        * `readwise_highlight_id` (text)
        * `note` (text)
        * `highlighted_at` (date)
        * `source` (**RELATION** -> Sources database)
      </Step>

      <Step title="Create the Concepts database">
        Title: "Reading Garden — Concepts". Properties:

        * `concept_name` (text)
        * `epistemic_status` (select: seedling / budding / evergreen)
        * `confidence` (number)
        * `mention_count` (number)
        * `last_updated` (date)
        * `highlights` (**RELATION** -> Highlights database)
      </Step>

      <Step title="Capture the three data_source_id values">
        ```bash theme={null}
        fracta spawn --task get-ds-ids \
          --contract "Call notion-search for each of 'Reading Garden — Sources', 'Reading Garden — Highlights', 'Reading Garden — Concepts'. For each hit, call notion-fetch and report the data_source_id."
        fracta peek get-ds-ids
        ```

        Paste these into the [First Run](/docs/patterns/reading-garden/first-run) prompt as `notion_sources_database_id`, `notion_highlights_database_id`, and `notion_concepts_database_id`.
      </Step>
    </Steps>
  </Tab>
</Tabs>

<Note>
  **No extra sharing step.** The OAuth grant you completed in Step 1 gives the fracta gateway the same Notion permissions your user has in the workspace you authorised — including the ability to create databases, read and write pages, and follow relations. Anything you (the user who completed OAuth) can do in your workspace, the gateway can do. There is no per-database "Connections" / "Add connections" / integration-sharing step.
</Note>

<Note>
  **Why three databases?** The v3 architecture mirrors the graph's three-tier `DomainSource -> Document -> Highlight` into Notion, with `Concept` as a separate dimension. Each Concept page links to the highlights that triggered it; each highlight links to its source book/article. This is what makes Notion useful as a reading garden rather than a flat dump of concept pages — you navigate from a concept back into its evidence.
</Note>

## Step 3 — Add the concept-extractor MCPs

The three extractor servers run as containers shipped from [`fracta-mcp-servers`](https://github.com/darkquasar/fracta-mcp-servers). They speak `streamable-http` on port `8000` at path `/mcp`, require no auth at the MCP layer (the gateway envelope handles identity), and each expose a single tool: `keybert_extract_tool`, `gliner_extract_tool`, and `spacy_extract_tool` respectively.

In Kubernetes, each runs as a Deployment + Service in the `fracta` namespace. Add (or confirm) these entries under `mcp_servers.servers:` in the gateway ConfigMap so the gateway can route to them by Service DNS:

```yaml theme={null}
mcp_servers:
  servers:
    # notion and readwise from Step 1 remain above

    concept-keybert:
      remote:
        url: http://concept-keybert.fracta.svc:8000/mcp
        transport: streamable-http

    concept-gliner:
      remote:
        url: http://concept-gliner.fracta.svc:8000/mcp
        transport: streamable-http

    concept-spacy:
      remote:
        url: http://concept-spacy.fracta.svc:8000/mcp
        transport: streamable-http
```

The corresponding Deployment + Service manifests live under `deployment/k8s/manifests/concept-*.yaml` if you scaffolded the pattern; otherwise pull them from [`fracta-mcp-servers`](https://github.com/darkquasar/fracta-mcp-servers).

<Warning>
  **Kubernetes readiness probes must be TCP, not `httpGet /mcp`.** The `streamable-http` transport returns HTTP 406 to plain GETs, which would mark the pod permanently unready. Use a `tcpSocket: { port: 8000 }` probe instead. The upstream manifests in [`fracta-mcp-servers`](https://github.com/darkquasar/fracta-mcp-servers) already follow this pattern; mirror it if you author your own.
</Warning>

<Note>
  **Session pinning matters.** `concept-gliner` loads \~1.4 GB of DeBERTa-v3 weights once per MCP session, in its `_lifespan` hook. The fracta gateway must pin `mcp-session-id` to the same upstream pod for the lifetime of a session, or every call reloads the model (10–30 s per request — the strategy becomes unusable). The default gateway configuration already pins; if you replace it with a custom load balancer, preserve sticky sessions.
</Note>

## Step 4 — Verify the stack

Three commands and a sanity-check spawn. By the end of this section you should see five servers reachable through the gateway, all returning their expected tool inventories.

```bash theme={null}
fracta debug gateway policy --verbose
```

Expected output: a `Tools:` block listing `notion.notion-search`, `readwise.list_highlights`, `concept-keybert.keybert_extract_tool`, `concept-gliner.gliner_extract_tool`, `concept-spacy.spacy_extract_tool` among the 27+ visible tools, all prefixed with `+` (allowed). If a server is missing, check its `mcp_servers.servers:` entry in the gateway ConfigMap. If a tool is prefixed `-` `[denied_by_policy]`, your `tool_policy:` block is restricting it.

Confirm each hosted MCP returns a real response:

```bash theme={null}
fracta spawn --task verify-notion \
  --contract "Call notion-search with query='setup smoke' and report whether the call succeeded (any result count is fine; we are checking auth, not content)."
fracta peek verify-notion
```

```bash theme={null}
fracta spawn --task verify-readwise \
  --contract "Call list_highlights with page_size=1 and report the highlight's id and book_id."
fracta peek verify-readwise
```

Both should return a real response, not an authentication error. If `verify-readwise` returns a 429, you have hit the **list-highlights 20-req/min** ceiling — wait one minute and retry. The `highlight-distill` strategy paginates against this same limit using a `watermark_iso` parameter, so the first real run uses a recent watermark to keep total request count bounded.

<Note>
  **Notion API limit: 3 req/s.** The hosted Notion MCP enforces the documented Notion REST limit. The `notion-publish` strategy spaces calls accordingly, but ad-hoc batch operations are easy to overshoot. As a rough number, publishing 100 Concept pages takes about 2.5 minutes wall-clock once retries and backoff are factored in.
</Note>

Finally, confirm the extractors are reachable through the gateway:

```bash theme={null}
fracta spawn --task verify-extractors \
  --contract "Call each of keybert_extract_tool, gliner_extract_tool, and spacy_extract_tool with text='Karl Popper argued for falsifiability as the demarcation criterion of science.' and report whether each call returned a non-empty result."
fracta peek verify-extractors
```

If any extractor times out or returns an empty list, check pod health (`kubectl get pods -n fracta`) and confirm the gateway can resolve the service DNS from its own container.

## What's next

<Cards>
  <Card title="Ontology" href="/docs/patterns/reading-garden/ontology">
    The eight node types the pipeline writes and the edges that connect them.
  </Card>

  <Card title="First Run" href="/docs/patterns/reading-garden/first-run">
    Spawn one Claude worker with one pipeline prompt; publish your first Concept page to Notion.
  </Card>
</Cards>
