Skip to main content
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; this page is purely the wiring.
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.

Prerequisites

1

Fracta installed and a runtime configured

Follow Installation and Your First Agent. Confirm fracta spawn --task hello --contract "say hi" returns cleanly before continuing.
2

A Readwise (or Reader) account with at least one synced highlight

Free accounts work. The pipeline reads through the hosted Readwise MCP — no API key is needed; fracta handles the OAuth grant.
3

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.
4

A Kubernetes cluster reachable from your laptop

The reading-garden pattern targets the Kubernetes deployment (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.
5

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.
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.

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:
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):
# 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:
# 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
# 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:
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.
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.
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.

1.3 — Export the tokens as Kubernetes Secrets

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.
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.

1.4 — Apply the Secrets and pick them up

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:
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).
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.

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.
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.
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.

Step 3 — Add the concept-extractor MCPs

The three extractor servers run as containers shipped from 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:
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.
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 already follow this pattern; mirror it if you author your own.
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.

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.
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:
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
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.
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.
Finally, confirm the extractors are reachable through the gateway:
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