# F(SYNC) agent guide — MCP servers

THIS DOCUMENT IS CANONICAL AND ALWAYS CURRENT: it is served by the
F(SYNC) auth service and updated in the same deploys that change the
platform. Do not rely on cached or pasted copies — re-fetch
https://fabric-sync.com/agents.md whenever in doubt.

Credentials: personal `mk_` API keys are minted on
https://fabric-sync.com/profile (a rotating key, plus one optional
permanent key that survives rotation until revoked — use the permanent
key for connectors). Email-OTP login at https://fabric-sync.com/ issues
24h `st_` sessions. Never guess or invent credentials; when one is
missing, ask the user.

## THE GATEWAY — one connection for everything (recommended)

```
https://fabric-sync.com/mcp
```

One mk_ key, every server: the gateway aggregates all F(SYNC) MCP
servers and namespaces their tools as `<server>__<tool>` —
`workspace__list_files`, `erp__get_item`, `meetings__list_meetings`,
`atlas__get_product_reviews`, ….
It forwards your credential to each server; no tool ever needs a
password argument.

### Identity — `whoami`
The gateway exposes native (un-namespaced) tools; `whoami` returns
`{ email }` — the F(SYNC) account the presented mk_ key belongs to.
Call it to answer "who am I logged in as?", to attribute actions, or to
confirm the connection is on the intended account. This is the contract
for identity — do not infer the account from space owners or other
tool output. `features` returns the map of every server behind the
gateway.

### Brand assets — `brand_assets`
The other native tool. It returns every logo variant with its public URL
— `icon` (the mark alone), `wordmark` (the type alone) and `lockup`
(both), each on transparent / royal blue `#1B3EBF` / gray `#DCDDDD` /
gray-blue `#8094AF` — plus the Long Advance and Yushan marks, the brand
colours, and the small ready-to-use copies the apex serves
(`/email-logo.png`, `/mcp/icon.png`). Call it whenever a page, deck,
email or favicon needs a logo; never guess a logo URL. The same list is
public over HTTP at
`https://fabric-sync.com/api/brand-assets` (`?format=md`,
`?brand=`, `?kind=`) — no key needed, so a browser or a curl can use it
too. The bytes live in the estate media bucket at
`web-assets.fabric-sync.com/brand/…`; the masters are 2363px, so
downscale before shipping one to a page.

Key placement (all servers accept the same three forms):
- `Authorization: Bearer mk_…` header (CLI agents)
- `?key=mk_…` on the URL (URL-only clients)
- `/mcp/k/<mk_key>` path segment (clients that strip query strings)

### Install — Claude Code
```
claude mcp add --transport http fsync-mcp https://fabric-sync.com/mcp --header "Authorization: Bearer <mk_ key>"
```

### Install — Codex CLI
```
codex mcp add fsync-mcp --url "https://fabric-sync.com/mcp?key=<mk_ key>"
```
Older Codex versions without native streamable-HTTP use the stdio bridge:
`codex mcp add fsync-mcp -- npx -y mcp-remote "https://fabric-sync.com/mcp?key=<mk_ key>"`

### Install — claude.ai web (custom connector)
1. Settings → Connectors → Add custom connector
2. Name: `fsync-mcp` · URL: `https://fabric-sync.com/mcp?key=<mk_ key>`
   (or the path form `https://fabric-sync.com/mcp/k/<mk_ key>`)
3. Under "Advanced settings" the dialog shows `OAuth Client ID
   (Optional)` and `OAuth Client Secret (Optional)` — ignore both and
   leave them empty; there is no OAuth, the key in the URL is the entire
   authentication. Use the PERMANENT key so the connector survives key
   rotation.
4. If a "couldn't register with sign-in service" toast appears, it is
   harmless discovery noise — dismiss it and press Connect; the
   connection completes without OAuth. The URL is the credential: do not
   share it, and revoke the key on /profile if it leaks.

### Install — ChatGPT (custom connector)
Needs a paid plan with **Developer mode** on (Settings → Connectors →
Advanced). Verified working 2026-07-28.
1. Settings → Connectors → Create
2. Name: `fsync-mcp` · Server URL:
   `https://fabric-sync.com/mcp/k/<mk_ key>` (path form — ChatGPT
   drops `?key=` query strings)
3. Authentication: **No Auth** — not OAuth. The key in the URL is the
   whole authentication.
4. Tick "I understand and want to continue" → Create. Use the PERMANENT
   key; the URL is the credential, so do not share it.

Deep research mode only accepts connectors exposing `search`/`fetch`
tools — use this one from a normal chat.

### Try asking
- "我現在是用哪個帳號登入?" / "who am I logged in as?" (whoami)
- "查 LNT-6112 的規格和庫存,還有哪些客戶在買" (erp__resolve_tokens → erp__get_item)
- "幫我在行事曆放 7/29 下午三點 跟 Sharon 對 DPP 欄位" (workspace__create_event)
- "list my workspace docs and summarize the latest one" (workspace__list_docs → workspace__get_doc)
- "0717 那場會議的 focus thread 說了什麼?" (meetings__list_meetings → meetings__get_thread)
- "消費者評論裡布料本身最常被抱怨什麼?" / "what do reviews say about the Ibex pant?" (atlas__list_review_themes attribution=fabric → atlas__get_product_reviews)
- "check my fsync inbox" / "把這份規格連同附件寄給 sharon@fsyncs.com" (workspace__list_threads / workspace__send_message)
- "sharon 對規格範圍回了什麼?幫我回覆並附上這兩張截圖" (workspace__get_thread → workspace__reply_message with `attachments`)
- "把 kevin 也拉進這個討論" (workspace__add_participant — he gets the whole history)
- "接著 thread_1f0c… 繼續" (workspace__get_thread on the pasted handle, then reply)
- "把這次工作的摘要寄到我的 fsync 信箱" (whoami → workspace__send_message to yourself — the agent-handoff pattern)
- "find alternatives to LNT-4040 and who bought them" (erp__get_item_alternatives → erp__get_customer_profile)
- "給我 F(SYNC) logo" / "put our logo on this deck cover" (brand_assets — transparent lockup for a light cover, royal-blue icon for a tile)

The per-server endpoints below remain directly connectable (same key
forms) when an agent only needs one domain.

## fsync-workspace

Workspace: private file storage, markdown docs, calendars, and a per-account mailbox of threaded discussions, organized in spaces (a space is the unit of sharing; every account has a private Personal space; owners share other spaces per member email; the mailbox is per-account, not per-space). A conversation is a THREAD any number of F(SYNC) accounts can be on, carrying attachments both ways — it is where humans and agents work a scope out between them. Web UI: https://workspace.fabric-sync.com

MCP endpoint (Streamable HTTP, stateless):
```
https://workspace-api.fabric-sync.com/mcp
```

### Auth
- Personal mk_ API key ONLY — `Authorization: Bearer mk_…` header, or `?key=mk_…` on the URL for clients that cannot send headers (claude.ai / ChatGPT custom connectors — pick "No Auth" there; there is no OAuth).
- Cookies and st_ session tokens are rejected with 401. Keys are minted on https://fabric-sync.com/profile (rotating or the single permanent key).

### Tools
- `list_spaces {}` — spaces I own / was shared into (+ member emails on my own)
- `list_files {space?}` — the files in a space this account may see (id, name, size, uploader; default space = Personal) — each row also reports its access: visibility ('space' | 'restricted'), shareable (false = the bytes may not leave the space, so it cannot be mailed as an attachment) and can_manage. Files restricted away from this account are absent, not marked
- `set_file_access {file_id, visibility?, shareable?, grant?[], revoke?[]}` — change ONE file's access — who inside the space sees it, and whether it may leave as a mail attachment. Uploader / space owner / admin only; grantees must already be members of the space
- `list_docs {space?}` — markdown docs metadata (id, title, size, author, timestamps)
- `get_doc {id}` — one doc in full (raw markdown)
- `create_doc {space?, title, content}` — create a markdown doc (≤500 KB)
- `update_doc {id, title?, content?}` — partial update (content = full replacement)
- `delete_doc {id}` — delete a doc
- `list_events {space?, from?, to?}` — calendar events, optionally bounded on start
- `create_event {space?, title, start, end?, description?, location?, url?, all_day?}` — start = 'YYYY-MM-DD' (all-day) or 'YYYY-MM-DD HH:MM', Asia/Taipei wall time
- `update_event {id, …partial}` — update supplied fields only
- `delete_event {id}` — delete an event
- `list_users {}` — all registered F(SYNC) accounts (email + display name) — the valid participants and send_message recipients
- `list_threads {box?, unread_only?, limit?, offset?}` — my conversations, unread first — the mailbox's main view. box = inbox (default) | sent | starred | all. Each row: participants, newest sender + snippet, message count, my unread count, thread_ handle
- `get_thread {id}` — the whole discussion in order — every message with author, time, markdown body and attachment ids, plus participants and read watermarks. Takes a thread handle OR any message handle from it. READ THIS BEFORE REPLYING
- `start_thread {participants[], subject, body?, format?, attachments?, file_ids?}` — open a discussion with up to 9 other registered accounts; returns its thread_ handle to quote back to the user
- `reply_message {id, body?, format?, attachments?, file_ids?, in_reply_to?}` — add my turn — THE tool for back-and-forth. Takes any handle from the thread; subject and audience come from the thread. Everyone else is emailed a link back
- `add_participant {id, email}` — bring someone into the discussion — they get the WHOLE history (use this, not forward, when they should join)
- `leave_thread {id, email?}` — drop myself (or, with email, someone else) from a discussion
- `mute_thread {id, muted?}` — stop/resume that thread's notification emails, for me only
- `send_message {to, subject, body?, format?, attachments?, file_ids?}` — 1:1 mail to another registered F(SYNC) account (self-send allowed) — opens a thread of two; ≤4 attachments — inline base64 (≤8 MB base64 each) and/or existing f_ file ids (bodies copied). Attachment bodies are stored in R2 and returned as download URLs
- `list_inbox {unread_only?, limit?, offset?}` — FLAT view: individual messages sent to me, unread first, with the total unread count. Prefer list_threads — every row here reports its thread_uuid
- `list_sent {limit?, offset?}` — FLAT view: messages I sent (read_at = read receipt)
- `get_message {id}` — ONE message in full + attachment download URLs; marks read, and reports the thread_uuid to open with get_thread
- `view_attachment {id, attachment_id}` — SEE a received image — returns the picture itself as image content (≤4 MB); the only way a mailed screenshot enters an agent's context, since attachment URLs are credentialed and most clients cannot fetch them
- `save_attachment {id, attachment_id, space?, folder?}` — copy a received attachment into my file storage (default Personal) as a regular workspace file
- `forward_message {id, to, subject?, body?, include_attachments?}` — hand a COPY of one message to someone OUTSIDE the discussion, as a new thread; add_participant is what brings them into this one
- `delete_message {id}` — remove MY copy of that message's thread; everyone else keeps theirs, storage is reclaimed when the last participant deletes

### Install
```
Claude Code:
  claude mcp add --transport http fsync-workspace https://workspace-api.fabric-sync.com/mcp --header "Authorization: Bearer <mk_ key>"
claude.ai web (Settings → Connectors → Add custom connector; the form takes only a URL):
  Name: F(SYNC) Workspace
  URL:  https://workspace-api.fabric-sync.com/mcp?key=<mk_ key>
```

### Notes
- Every doc has a rendered HTML preview at https://workspace-api.fabric-sync.com/api/docs/<id>/preview (same credential).
- File bodies are not exposed over MCP — download via https://workspace-api.fabric-sync.com/api/files/<id> with the same key.
- Access rule everywhere: space owner or listed member. Nothing is anonymous.
- Mailbox exception: conversations are per-account, not per-space. ONE access rule — you can read a message iff you are a participant of its thread; attachment downloads are authorized the same way, with no space membership involved.
- Working a discussion: list_threads → get_thread (read what has already been said) → reply_message. Answering from a subject line without reading the thread is how an agent repeats a point that was settled two turns ago. A `thread_…` a user pastes means "carry on with this"; a `mail_…` still resolves — every message reports the thread it belongs to.
- Two message forms: send_message / start_thread / reply_message take an optional `format` — 'markdown' (default: persists in the thread, others get a short nudge email with a link) or 'email' (persists the same, but the full Markdown body is also rendered to HTML and delivered as a real email to each participant's inbox). If the user has not said which form and it matters, ask; otherwise default to 'markdown'.
- Every reply emails the other participants a link back to the thread, so replying is how you reach a human who is not currently in the app. It is debounced (one email per person per thread per 15 minutes) and silent for anyone who muted the thread.
- Screenshot handles: in a browser chat client (claude.ai / ChatGPT) the F(SYNC) Chrome extension uploads every image the user pastes into the composer and writes an `f_…` handle beside it. That handle IS the screenshot — pass it in send_message `file_ids` (or save it with save_attachment) instead of describing the picture. It is the only way a pasted image can reach an MCP tool: MCP carries the model's JSON, not the conversation's pixels.
- Relaying a pasted chat session: the screenshots pasted with it are PART of the message — base64 them into `attachments` (bodies land in R2, returned as download URLs) rather than sending the text alone. This holds identically for send_message, start_thread and reply_message: a scope discussion is mostly screenshots, and a reply that drops them is not the reply that was asked for. Bytes you can reach (file path, fetchable URL, workspace f_ id) must be attached; only an image that exists solely as a pasted image you cannot read may be transcribed into the body instead, and then say so in the message.
- Agent handoff pattern: to leave the user a copy/summary of work done in ANY MCP-connected environment, send it to their own mailbox — whoami (gateway) for the email, then send_message to that address with the summary as markdown body (attach files if useful). It shows up as unread in the workspace UI and in list_inbox everywhere else.

## la-erp

Read-only lookup over the LA ERP estate (textile trader Long Advance): items (品號) with specs, orders, customers, substitute fabrics, and trading history. Commercial data — quote it, don't bulk-export it. The full ERP app lives at https://erp.fabric-sync.com

MCP endpoint (Streamable HTTP, stateless):
```
https://la-erp-chrome.fabric-sync.com/mcp
```

### Auth
- Personal mk_ API key ONLY — `Authorization: Bearer mk_…` header, or `?key=mk_…` on the URL for clients that cannot send headers (claude.ai / ChatGPT custom connectors — pick "No Auth" there; there is no OAuth).
- Cookies and st_ session tokens are rejected on /mcp. Keys are minted on https://fabric-sync.com/profile.

### Tools
- `resolve_tokens {tokens}` — classify free-text tokens: item numbers (品號) vs order document numbers vs spec-card codes — start here for unknown references
- `get_item {item_no}` — item master + spec (composition, construction, weight, width, finish) and trading summary
- `get_item_alternatives {item_no, limit?}` — similar/substitute fabrics ranked by fibre composition
- `get_order {doc_no}` — order header, lines, shipment state
- `find_customer {name? | email?}` — companies by name fragment, or by contact email (address → domain fallback); provide exactly one
- `get_customer_profile {company_no, item_no?}` — a customer's trading profile, optionally focused on one item

### Install
```
Claude Code:
  claude mcp add --transport http la-erp https://la-erp-chrome.fabric-sync.com/mcp --header "Authorization: Bearer <mk_ key>"
claude.ai web (Settings → Connectors → Add custom connector):
  Name: F(SYNC) LA-ERP
  URL:  https://la-erp-chrome.fabric-sync.com/mcp?key=<mk_ key>
```

### Notes
- Read-only by design; drafts/writes stay in the ERP app. The raw SQL gateway is never exposed over MCP.

## meetings-static

Meeting program: the registry of fsync meeting-note sites, participant threads (comments / focus), labels, and the deployed audio pipeline. Notes served at https://meetings.fabric-sync.com/<slug>/{en,zh-Hant}/

MCP endpoint (Streamable HTTP, stateless):
```
https://meetings.fabric-sync.com/mcp
```

### Auth
- Per-call credential in the `password` ARGUMENT of every tool: the feature access password, a personal mk_ API key, or a 24h st_ session token.
- When no credential is at hand, ASK THE USER — never guess or reuse one from elsewhere. Users sign in at https://fabric-sync.com/ (email OTP).

### Tools
- `list_meetings {password}` — registry: slug, date, bilingual title/brief
- `get_thread {password, kind, slug}` — read a thread; kind = 'comments' | 'focus'
- `post_entry {password, kind, slug, name, text}` — add an entry (signed-in users post under their profile name)
- `list_labels {password} / get_meeting_context {slug} / assign_label / assign_labels_batch` — label system — agents ASSOCIATE existing labels only, never create; confirm mappings with the user first
- `list_runs / get_progress / list_voice_samples / submit_stage_b_config / submit_slug` — audio pipeline — runs pause at 'awaiting-roster' and 'awaiting-slug' for agent input; see the server's own instructions

### Install
```
Claude Code:
  claude mcp add --transport http meetings-static https://meetings.fabric-sync.com/mcp
Codex CLI (stdio bridge, all versions) — ~/.codex/config.toml:
  [mcp_servers.meetings-static]
  command = "npx"
  args = ["-y", "mcp-remote", "https://meetings.fabric-sync.com/mcp"]
Codex CLI (native streamable-HTTP, newer versions):
  codex mcp add meetings-static --url https://meetings.fabric-sync.com/mcp
```

### Notes
- The server's initialize instructions carry the full behavioral contract (label etiquette, pipeline pause points) — read them.

## fsync-web-assets

Management plane for the estate's public media bucket (served at https://web-assets.fabric-sync.com/<area>/<file>) plus an upload-request queue: file a request for an asset of any size (10 GB+ included), and the agent on the machine holding the file claims it and uploads with plain curl — no wrangler, no Cloudflare credentials. Manager UI for humans: https://assets.fabric-sync.com

MCP endpoint (Streamable HTTP, stateless):
```
https://assets.fabric-sync.com/mcp
```

### Auth
- Personal mk_ API key ONLY — `Authorization: Bearer mk_…` header, or `?key=mk_…` on the URL for clients that cannot send headers (claude.ai / ChatGPT custom connectors — pick "No Auth" there; there is no OAuth).
- The account must also hold the `web-assets` RBAC grant (admins pass implicitly). Keys are minted on https://fabric-sync.com/profile.

### Tools
- `status` — caller identity + queue counts + bucket/public-base info
- `list_assets {prefix?, cursor?, flat?}` — browse the bucket by area; flat:true recurses
- `get_asset {key} / delete_asset {key} / purge_url {keys?, urls?}` — object metadata + public URL; delete (auto-purges); force-refresh CDN copies
- `list_requests {status?} / get_request {id}` — the upload-request queue and one request's full event history
- `create_request {target_key, source_type, source_url?, content_type?, size_bytes?, notes?}` — file an upload: source 'local-file' (an agent claims + uploads) or 'url' (server-side ingest)
- `claim_request {id}` — compare-and-swap claim; the response is ready-to-run curl (single PUT ≤95 MB, 64 MB-part multipart past it) needing only the caller's mk_ key
- `complete_request {id} / fail_request {id, error}` — verify-and-close after an out-of-band upload; or release the row for re-claim
- `ingest_url {id}` — server-side execution of a url-sourced request — holds the call until the transfer finishes (multi-GB takes minutes)

### Install
```
Claude Code:
  claude mcp add --transport http fsync-web-assets https://assets.fabric-sync.com/mcp --header "Authorization: Bearer <mk_ key>"
claude.ai web (Settings → Connectors → Add custom connector):
  Name: F(SYNC) Web assets
  URL:  https://assets.fabric-sync.com/mcp?key=<mk_ key>
```

### Notes
- The upload endpoints under https://assets.fabric-sync.com/api/* accept the same mk_ key as a bearer — claim_request prints the exact commands, including the multipart loop for 10 GB+ files.
- Every completed write purges the CDN copy of its public URL automatically (an R2 overwrite never refreshes the CDN by itself).
- Heavy web media belongs HERE, never in a repo or a worker's public/ (estate rule) — key prefix = owning area, e.g. meshy/.

## willeb-sites

Publish a static site to <slug>.willeb.com and it is live at once — no build step, no wrangler, no Cloudflare credentials on the calling machine. Files go straight into the call (text or base64); every deploy is a kept version, so rollback is a pointer move. NOT a fabric-sync.com host: WILLEB is a separate venture on its own zone that borrows this estate's identity, so the mk_ key works but the SSO cookie never reaches it. The apex willeb.com is not deployable here — it belongs to the main WILLEB site on its own worker.

MCP endpoint (Streamable HTTP, stateless):
```
https://deploy.willeb.com/mcp
```

### Auth
- Personal mk_ API key ONLY — `Authorization: Bearer mk_…` header, or `?key=mk_…` on the URL for clients that cannot send headers (claude.ai / ChatGPT custom connectors — pick "No Auth" there; there is no OAuth).
- The account must also hold the `willeb` RBAC grant (admins pass implicitly). Keys are minted on https://fabric-sync.com/profile.

### Tools
- `status / list_sites / get_site {slug}` — caller identity + limits; every site with its live version; one site's files and version history
- `deploy_site {slug, files[], base?, remove?, title?, spa?, notes?}` — THE call: creates the site if needed, stores the files as a new version and makes it live. base:'live' starts from what is published and applies only what you send
- `create_deployment {slug, base?} / add_files {deployment_id, files[]} / remove_files / publish_deployment {deployment_id}` — the staged path, for sites too big for one call
- `upload_command {deployment_id, path}` — ready-to-run curl for one large binary (≤95 MB), needing only the caller's mk_ key
- `rollback_site {slug, version?} / set_site {slug, …} / delete_site {slug, confirm}` — restore an earlier version instantly; change title/SPA/notes; destroy a site and all its bytes
- `list_apps / create_app_deployment {app} / publish_app {deployment_id}` — the WORKER path, for apps rather than static sites (the main WILLEB site on the apex): stage a built dist/ with add_files, then publish_app uploads it to Cloudflare — script name and bindings come from the plane's registry, never from the uploaded wrangler.json

### Install
```
Claude Code:
  claude mcp add --transport http willeb https://deploy.willeb.com/mcp --header "Authorization: Bearer <mk_ key>"
claude.ai web (Settings → Connectors → Add custom connector):
  Name: WILLEB deploys
  URL:  https://deploy.willeb.com/mcp?key=<mk_ key>
Or take no action at all: the F(SYNC) gateway at https://fabric-sync.com/mcp already carries these tools as willeb__*.
```

### Notes
- A file is `{path, text}` for source or `{path, base64}` for binary; content type comes from the extension. One call carries 6 MB decoded — past that use create_deployment + upload_command.
- A deploy that hits an error on ANY file is not published: the draft survives and add_files + publish_deployment finishes it, so a half-written site never replaces a working one.
- A publish is visible within 30 s worldwide (the site→version pointer is cached that long per isolate); the bytes themselves are immutable and shared between versions.
- Worker apps: stage `/server/**` (modules + wrangler.json) and `/client/**` exactly as built. publish_app does the three-step Cloudflare upload (assets session → buckets → script) and holds the call while it transfers; assets already stored are reused, so a rebuild usually uploads only what changed.

## fsync-context

The estate's JSON context catalogue: every JSON data file in the fsync monorepo classified by what it MEANS — category, the subject it is about, a field map with real sample values, record counts — plus the raw bodies, stored by content hash in KV/R2 so identical copies staged in several projects are held once. It answers 'where does this data live and what is in it' without a repo checkout.

MCP endpoint (Streamable HTTP, stateless):
```
https://context.fabric-sync.com/mcp
```

### Auth
- Personal mk_ API key ONLY — `Authorization: Bearer mk_…` header, or `?key=mk_…` / `/mcp/k/<key>` for clients that cannot send headers.
- The account must also hold the `context` RBAC grant (admins pass implicitly). Keys are minted on https://fabric-sync.com/profile.

### Tools
- `status` — catalogue size, unique blobs, where raw bodies live, last ingest
- `list_categories` — THE MAP — every semantic category, what a file in it means, what its subject names
- `list_projects {area?, q?}` — projects owning JSON, with bytes, categories and the code that reads them
- `find_sources {category?, project?, area?, subject?, variant?, field?, q?, limit?, offset?}` — filter the catalogue; variant separates live data from pipeline staged/backup twins
- `get_source {id}` — one file in full: category meaning, complete field map with samples, identical copies elsewhere
- `search {q, limit?}` — free-text sweep over paths, subjects, keys and field names
- `lookup_field {path, limit?}` — concept → data: which JSON paths match, in how many sources, with a real sample per path
- `get_raw {id, pointer?, offset?, length?, max_chars?}` — the actual JSON; bodies over ~6 MB can only be byte-sliced, never parsed in-worker
- `ask {question, limit?}` — plain question → ranked candidate sources scored on categories, subjects, field paths and full text

### Install
```
Claude Code:
  claude mcp add --transport http fsync-context https://context.fabric-sync.com/mcp --header "Authorization: Bearer <mk_ key>"
claude.ai web (Settings → Connectors → Add custom connector):
  Name: F(SYNC) Context
  URL:  https://context.fabric-sync.com/mcp?key=<mk_ key>
```

### Notes
- Read get_source's field map BEFORE get_raw — a 105 MB brand catalogue cannot be parsed in a Worker isolate, only sliced.
- Unless the question is about the collection pipeline itself, filter variant='live': the same catalogue also exists as staged inputs and promotion backups.
- The catalogue is rebuilt by core/fsync-context-wkr/scripts/pipeline.mjs — re-run it after the estate's JSON changes; ids are stable, so it is idempotent.
