# cambium (remote) [Health: Active]

**Category:** ☁️ Cloud Platforms  
**Repository:** https://github.com/jarmstrong158/cambium-remote  
**GitHub Stars:** 0  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/cambium-remote

## Description
Cloudflare Worker for read-only recall of cambium knowledge. MCP 2026-07-28, dual-era.

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `npx` (confidence: high):

```json
"mcpServers": {
  "cambium-remote": {
    "command": "npx",
    "args": ["-y","wrangler"]
  }
}
```

## Documentation & README

# cambium-remote

_Part of the [xylem](https://github.com/jarmstrong158/xylem) stack._

A Cloudflare Worker MCP server that makes **cambium's promoted knowledge**
recallable from **claude.ai (including mobile)** — read-only.

Local cambium is a desktop stdio server; its **team** knowledge lives on a
`cambium` branch of each project repo and its **org** knowledge lives in a
dedicated knowledge repo — both plain `knowledge.json` files in git. This Worker
reads those files through the GitHub Contents API and serves `recall()` over
them, the same pattern [`agentsync-remote`](https://github.com/jarmstrong158/agentsync-remote)
uses for the coordination board.

**Read-only by design.** It exposes `recall` and `status`. It does **not**
`distill`, `endorse`, or `promote` (those are CAS writes / the generalization
gate — desktop-only), and recall here does **not** increment recall counts (so
it never feeds promotion). Local (personal, unpromoted) scope is desktop-only
and not reachable remotely — only the promoted **team** and **org** tiers are.

## Tools

- **`recall(query, scope?, limit?)`** — search team + org knowledge. `scope`:
  `auto` (default, team+org) | `team` | `org`. Abstains with
  `no_confident_match` below the relevance floor, exactly like local cambium.
- **`status()`** — what the Worker is configured to read and how many active
  items each scope holds. Call it first if recall looks empty.

## ⚠️ Trust model — read this before pointing it at an org

**Team "scope" is a read selector, not an authorization control.** It answers
*"where do we look"*, not *"who is allowed to tell us things"*. Nothing in this
Worker checks provenance, signatures, or authorship of the knowledge it serves.

In the default `TEAM_SCOPE_MODE = "discover"` mode, the entire membership test
for team scope is:

> does this repo, owned by `TEAM_OWNER`, have a branch named `TEAM_BRANCH`?

There is **no allowlist and no denylist**. So:

- **Anyone who can push a `cambium` branch to *any* repo under `TEAM_OWNER`
  can inject into your org-wide recall** — an outside contributor with write
  access to one small repo, a compromised CI token, a collaborator you forgot
  to remove. Their `knowledge.json` is read as authoritative team knowledge.
- It takes effect **within the 5-minute discovery cache TTL**. No redeploy, no
  review, no notification.
- `recall()` output is exactly the kind of thing an agent treats as
  established fact and acts on. **This is an injection path into agent
  behaviour**, not merely a data-quality problem.

Discovery remains the default because it is genuinely safe in the
single-operator case this was built for, and because changing it silently would
empty team scope on existing deployments. **It is safe only if you are the only
person who can push under `TEAM_OWNER`.**

### If anyone else can push under `TEAM_OWNER`, use strict mode

```toml
TEAM_SCOPE_MODE = "allowlist"
TEAM_REPOS = "owner/repo-a,owner/repo-b"
```

Team scope then becomes **exactly** those repos. No discovery scan is performed,
and `TEAM_OWNER` is **ignored entirely** — so leaving it set in `wrangler.toml`
cannot silently re-widen your scope later. Adding a repo becomes a deliberate
config change.

`status()` reports which model is in force, in plain language, in its own
output. If you are unsure what a deployment is doing, call it.

### What `status()` deliberately does *not* tell you

By default `status()` returns the **count** of team repos, not their names. In
discover mode those names are the output of a scan over everything `TEAM_OWNER`
owns, so they enumerate **private repositories** — their existence and their
names — to anyone holding the path token, who has no GitHub identity and no repo
permissions of their own. The count answers the actual diagnostic question
("is team scope finding anything?"). Set `STATUS_DISCLOSE_REPOS = "true"` to opt
back in. In `allowlist` mode names are always shown: they are your own committed
config, not a discovery result.

### `GH_PAT` is the broadest credential in the xylem stack

Discovery requires **Metadata: Read + Contents: Read across every repo under
`TEAM_OWNER`**, including private ones. That makes this Worker's PAT
substantially more powerful than `agentsync-remote`'s (scoped to one
coordination repo). Two consequences worth acting on:

- Prefer `allowlist` mode, which lets you scope the PAT to **only** the listed
  repos instead of the whole account.
- Treat a leak of `AUTH_TOKEN` here as more serious than for the siblings: the
  Worker will read any in-scope repo on the caller's behalf.

## Configure (`wrangler.toml` vars)

| var | meaning |
|---|---|
| `ORG_REPO` | `owner/name` of the dedicated org knowledge repo (its default branch's `knowledge.json`). Blank = no org recall. |
| `TEAM_SCOPE_MODE` | `discover` (default) or `allowlist`. **A trust decision** — see [Trust model](#️-trust-model--read-this-before-pointing-it-at-an-org). |
| `TEAM_OWNER` | *discover mode only.* Owner (user/org) to auto-discover team repos under; every repo of theirs with `TEAM_BRANCH` is read. Blank = no discovery. **Ignored in `allowlist` mode.** |
| `TEAM_REPOS` | *discover mode:* optional extra `owner/name` repos on top of discovery. *allowlist mode:* the **complete** team-scope set, comma-separated. |
| `TEAM_BRANCH` | team-scope branch (default `cambium`). |
| `KNOWLEDGE_PATH` | file name (default `knowledge.json`). |
| `STATUS_DISCLOSE_REPOS` | `"true"` lets `status()` list team repo **names**. Default off — discovered names can include private repos. |

In `discover` mode team scope is a *growing* set: a newly team-promoted repo
shows up on mobile within minutes with no redeploy. One GraphQL scan per few
minutes (cached in-isolate) lists the repos; a repo without the branch is
skipped.

## Deploy

```bash
npm install
npm run typecheck && npm test
npx wrangler deploy
# then set the two secrets in the Cloudflare dashboard (never in the repo):
npx wrangler secret put AUTH_TOKEN   # the path-token credential; URL is /mcp/<AUTH_TOKEN>
npx wrangler secret put GH_PAT       # fine-grained GitHub token: Metadata: Read + Contents: Read across your repos
#                                    # (team scope is auto-discovered, so it needs to see all of TEAM_OWNER's repos)
```

Then add `https://cambium-remote.<subdomain>.workers.dev/mcp/<AUTH_TOKEN>` as a
custom connector in **claude.ai → Settings → Connectors**. The whole URL is the
credential — treat it like a password.

## 🔒 Auth — the connector URL is a credential

Path-token: `POST /mcp/<token>`, compared against the `AUTH_TOKEN` secret in
time independent of both content **and length**; anything else returns a bare
`404`. Same scheme, and now literally the same code
(`src/shared/mcp-core.ts`), as the sibling Workers.

### Why the token is in the URL path, and what that costs you

This is a **deliberate design choice, not an oversight**. claude.ai custom
connectors do not reliably send custom headers, so an `Authorization:` header —
the obvious alternative — cannot be depended on. Putting the credential in the
path is what makes the connector work at all.

Be clear about the price, because it is not the same as a header:

- **URLs get recorded in places request bodies never do.** Browser history,
  shell history, proxy and CDN access logs, crash reports, bug reports,
  screenshots, "copy link" buttons, and **agent session transcripts**. During
  the audit that produced this section, the connector URLs for these Workers
  were found in **~54 occurrences across 13 local session transcripts** on a
  single machine — none pasted deliberately; they were simply part of the tool
  configuration an agent echoed back.
- **The Worker itself does not log it.** Every log line records the route as
  `/mcp/***`. The leak surface is everything *around* the Worker, which is
  exactly what you cannot audit.
- **A leak here is the worst of the three**, because this Worker's `GH_PAT`
  spans every repo under `TEAM_OWNER` (see [Trust
  model](#️-trust-model--read-this-before-pointing-it-at-an-org)).

**Practical guidance:**

1. **Rotate on a schedule**, not just on suspicion — assume the URL has been
   recorded somewhere you don't control. Rotation is cheap: change `AUTH_TOKEN`,
   update the connector.
2. **Rotate immediately** if you've shared a terminal recording, a transcript,
   a screen capture, or a bug report from a machine where the connector is
   configured.
3. Use a **long random token** (32+ bytes, e.g. `openssl rand -hex 32`).
4. Scope `GH_PAT` as narrowly as your `TEAM_SCOPE_MODE` allows.
5. If you ever get the chance to use a header or OAuth instead, **take it**.
   This tradeoff is forced by the client, not preferred.

## Development

```bash
npm install
npm run typecheck
npm test
```

CI (`.github/workflows/ci.yml`) runs typecheck + tests on every push and PR,
including in forks. `.github/workflows/deploy.yml` deploys the canonical repo on
green `main` only, and is guarded by `if: github.repository == …` so a fork
never attempts a deploy it has no secrets for.

## License

MIT — see [LICENSE](https://github.com/jarmstrong158/cambium-remote/blob/HEAD/LICENSE), matching the sibling
[`agentsync-remote`](https://github.com/jarmstrong158/agentsync-remote) and
[`context-keeper-remote`](https://github.com/jarmstrong158/context-keeper-remote)
Workers.

> **Note for the maintainer:** this README previously stated PolyForm
> Noncommercial 1.0.0 while the repo shipped **no LICENSE file at all**. An MIT
> `LICENSE` has been added here to match both siblings. If PolyForm was the
> intended licence for this Worker, replace `LICENSE` and this section before
> merging — the discrepancy was pre-existing and is flagged rather than assumed.

