{
  "markdown": "# cambium-remote\n\n_Part of the [xylem](https://github.com/jarmstrong158/xylem) stack._\n\nA Cloudflare Worker MCP server that makes **cambium's promoted knowledge**\nrecallable from **claude.ai (including mobile)** — read-only.\n\nLocal cambium is a desktop stdio server; its **team** knowledge lives on a\n`cambium` branch of each project repo and its **org** knowledge lives in a\ndedicated knowledge repo — both plain `knowledge.json` files in git. This Worker\nreads those files through the GitHub Contents API and serves `recall()` over\nthem, the same pattern [`agentsync-remote`](https://github.com/jarmstrong158/agentsync-remote)\nuses for the coordination board.\n\n**Read-only by design.** It exposes `recall` and `status`. It does **not**\n`distill`, `endorse`, or `promote` (those are CAS writes / the generalization\ngate — desktop-only), and recall here does **not** increment recall counts (so\nit never feeds promotion). Local (personal, unpromoted) scope is desktop-only\nand not reachable remotely — only the promoted **team** and **org** tiers are.\n\n## Tools\n\n- **`recall(query, scope?, limit?)`** — search team + org knowledge. `scope`:\n  `auto` (default, team+org) | `team` | `org`. Abstains with\n  `no_confident_match` below the relevance floor, exactly like local cambium.\n- **`status()`** — what the Worker is configured to read and how many active\n  items each scope holds. Call it first if recall looks empty.\n\n## ⚠️ Trust model — read this before pointing it at an org\n\n**Team \"scope\" is a read selector, not an authorization control.** It answers\n*\"where do we look\"*, not *\"who is allowed to tell us things\"*. Nothing in this\nWorker checks provenance, signatures, or authorship of the knowledge it serves.\n\nIn the default `TEAM_SCOPE_MODE = \"discover\"` mode, the entire membership test\nfor team scope is:\n\n> does this repo, owned by `TEAM_OWNER`, have a branch named `TEAM_BRANCH`?\n\nThere is **no allowlist and no denylist**. So:\n\n- **Anyone who can push a `cambium` branch to *any* repo under `TEAM_OWNER`\n  can inject into your org-wide recall** — an outside contributor with write\n  access to one small repo, a compromised CI token, a collaborator you forgot\n  to remove. Their `knowledge.json` is read as authoritative team knowledge.\n- It takes effect **within the 5-minute discovery cache TTL**. No redeploy, no\n  review, no notification.\n- `recall()` output is exactly the kind of thing an agent treats as\n  established fact and acts on. **This is an injection path into agent\n  behaviour**, not merely a data-quality problem.\n\nDiscovery remains the default because it is genuinely safe in the\nsingle-operator case this was built for, and because changing it silently would\nempty team scope on existing deployments. **It is safe only if you are the only\nperson who can push under `TEAM_OWNER`.**\n\n### If anyone else can push under `TEAM_OWNER`, use strict mode\n\n```toml\nTEAM_SCOPE_MODE = \"allowlist\"\nTEAM_REPOS = \"owner/repo-a,owner/repo-b\"\n```\n\nTeam scope then becomes **exactly** those repos. No discovery scan is performed,\nand `TEAM_OWNER` is **ignored entirely** — so leaving it set in `wrangler.toml`\ncannot silently re-widen your scope later. Adding a repo becomes a deliberate\nconfig change.\n\n`status()` reports which model is in force, in plain language, in its own\noutput. If you are unsure what a deployment is doing, call it.\n\n### What `status()` deliberately does *not* tell you\n\nBy default `status()` returns the **count** of team repos, not their names. In\ndiscover mode those names are the output of a scan over everything `TEAM_OWNER`\nowns, so they enumerate **private repositories** — their existence and their\nnames — to anyone holding the path token, who has no GitHub identity and no repo\npermissions of their own. The count answers the actual diagnostic question\n(\"is team scope finding anything?\"). Set `STATUS_DISCLOSE_REPOS = \"true\"` to opt\nback in. In `allowlist` mode names are always shown: they are your own committed\nconfig, not a discovery result.\n\n### `GH_PAT` is the broadest credential in the xylem stack\n\nDiscovery requires **Metadata: Read + Contents: Read across every repo under\n`TEAM_OWNER`**, including private ones. That makes this Worker's PAT\nsubstantially more powerful than `agentsync-remote`'s (scoped to one\ncoordination repo). Two consequences worth acting on:\n\n- Prefer `allowlist` mode, which lets you scope the PAT to **only** the listed\n  repos instead of the whole account.\n- Treat a leak of `AUTH_TOKEN` here as more serious than for the siblings: the\n  Worker will read any in-scope repo on the caller's behalf.\n\n## Configure (`wrangler.toml` vars)\n\n| var | meaning |\n|---|---|\n| `ORG_REPO` | `owner/name` of the dedicated org knowledge repo (its default branch's `knowledge.json`). Blank = no org recall. |\n| `TEAM_SCOPE_MODE` | `discover` (default) or `allowlist`. **A trust decision** — see [Trust model](#️-trust-model--read-this-before-pointing-it-at-an-org). |\n| `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.** |\n| `TEAM_REPOS` | *discover mode:* optional extra `owner/name` repos on top of discovery. *allowlist mode:* the **complete** team-scope set, comma-separated. |\n| `TEAM_BRANCH` | team-scope branch (default `cambium`). |\n| `KNOWLEDGE_PATH` | file name (default `knowledge.json`). |\n| `STATUS_DISCLOSE_REPOS` | `\"true\"` lets `status()` list team repo **names**. Default off — discovered names can include private repos. |\n\nIn `discover` mode team scope is a *growing* set: a newly team-promoted repo\nshows up on mobile within minutes with no redeploy. One GraphQL scan per few\nminutes (cached in-isolate) lists the repos; a repo without the branch is\nskipped.\n\n## Deploy\n\n```bash\nnpm install\nnpm run typecheck && npm test\nnpx wrangler deploy\n# then set the two secrets in the Cloudflare dashboard (never in the repo):\nnpx wrangler secret put AUTH_TOKEN   # the path-token credential; URL is /mcp/<AUTH_TOKEN>\nnpx wrangler secret put GH_PAT       # fine-grained GitHub token: Metadata: Read + Contents: Read across your repos\n#                                    # (team scope is auto-discovered, so it needs to see all of TEAM_OWNER's repos)\n```\n\nThen add `https://cambium-remote.<subdomain>.workers.dev/mcp/<AUTH_TOKEN>` as a\ncustom connector in **claude.ai → Settings → Connectors**. The whole URL is the\ncredential — treat it like a password.\n\n## 🔒 Auth — the connector URL is a credential\n\nPath-token: `POST /mcp/<token>`, compared against the `AUTH_TOKEN` secret in\ntime independent of both content **and length**; anything else returns a bare\n`404`. Same scheme, and now literally the same code\n(`src/shared/mcp-core.ts`), as the sibling Workers.\n\n### Why the token is in the URL path, and what that costs you\n\nThis is a **deliberate design choice, not an oversight**. claude.ai custom\nconnectors do not reliably send custom headers, so an `Authorization:` header —\nthe obvious alternative — cannot be depended on. Putting the credential in the\npath is what makes the connector work at all.\n\nBe clear about the price, because it is not the same as a header:\n\n- **URLs get recorded in places request bodies never do.** Browser history,\n  shell history, proxy and CDN access logs, crash reports, bug reports,\n  screenshots, \"copy link\" buttons, and **agent session transcripts**. During\n  the audit that produced this section, the connector URLs for these Workers\n  were found in **~54 occurrences across 13 local session transcripts** on a\n  single machine — none pasted deliberately; they were simply part of the tool\n  configuration an agent echoed back.\n- **The Worker itself does not log it.** Every log line records the route as\n  `/mcp/***`. The leak surface is everything *around* the Worker, which is\n  exactly what you cannot audit.\n- **A leak here is the worst of the three**, because this Worker's `GH_PAT`\n  spans every repo under `TEAM_OWNER` (see [Trust\n  model](#️-trust-model--read-this-before-pointing-it-at-an-org)).\n\n**Practical guidance:**\n\n1. **Rotate on a schedule**, not just on suspicion — assume the URL has been\n   recorded somewhere you don't control. Rotation is cheap: change `AUTH_TOKEN`,\n   update the connector.\n2. **Rotate immediately** if you've shared a terminal recording, a transcript,\n   a screen capture, or a bug report from a machine where the connector is\n   configured.\n3. Use a **long random token** (32+ bytes, e.g. `openssl rand -hex 32`).\n4. Scope `GH_PAT` as narrowly as your `TEAM_SCOPE_MODE` allows.\n5. If you ever get the chance to use a header or OAuth instead, **take it**.\n   This tradeoff is forced by the client, not preferred.\n\n## Development\n\n```bash\nnpm install\nnpm run typecheck\nnpm test\n```\n\nCI (`.github/workflows/ci.yml`) runs typecheck + tests on every push and PR,\nincluding in forks. `.github/workflows/deploy.yml` deploys the canonical repo on\ngreen `main` only, and is guarded by `if: github.repository == …` so a fork\nnever attempts a deploy it has no secrets for.\n\n## License\n\nMIT — see [LICENSE](./LICENSE), matching the sibling\n[`agentsync-remote`](https://github.com/jarmstrong158/agentsync-remote) and\n[`context-keeper-remote`](https://github.com/jarmstrong158/context-keeper-remote)\nWorkers.\n\n> **Note for the maintainer:** this README previously stated PolyForm\n> Noncommercial 1.0.0 while the repo shipped **no LICENSE file at all**. An MIT\n> `LICENSE` has been added here to match both siblings. If PolyForm was the\n> intended licence for this Worker, replace `LICENSE` and this section before\n> merging — the discrepancy was pre-existing and is flagged rather than assumed.\n",
  "bytes": 9694,
  "sha": "ac50738e685ac166915597f2b061865cdf90d20bf26bab194f4dab5762bb085a",
  "repo_slug": "jarmstrong158/cambium-remote",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_jarmstrong158_cambium_remote_b77948bb/readme"
}