# /docs/start/quickstart # Fabric MCP — Quickstart Under 1 minute from `npx` to indexing your repo. ## Prerequisites - Node.js 18+ - One of the following MCP-compatible clients installed and signed in: - [Claude Code](https://docs.claude.com/en/docs/claude-code) - [Cursor](https://cursor.com/docs) - [Codex](https://developers.openai.com/codex/quickstart?setup=cli) ## Install ```bash npx -y @cognisos/fabric-mcp setup ``` The setup command signs you in via your browser and registers Fabric as an MCP server. > **Claude Code** — restart Claude Code and run `/mcp` to verify `fabric` is listed. > **Cursor** — restart Cursor; open chat, click the tools icon, and confirm `fabric.*` tools appear. > **Codex** — restart your Codex session; Fabric tools will be available automatically. ## Verify Confirm the tools are registered in your client (see the client note above), then in any project ask your AI assistant to run: ``` fabric_status ``` You should see node/edge counts for the local index. ## First workflow Ask your AI assistant to: > Use `fabric_index` to index this project. Indexing takes a few seconds for a small repo. The local index lives under `~/.fabric/index/v2/`. If anything looks off, run `npx @cognisos/fabric-mcp doctor`. --- # /docs/fabric-indexer # Fabric Indexer **MCP server that turns any codebase into a typed knowledge graph for AI assistants.** ## What it is `fabric-indexer` is a local-first [Model Context Protocol](https://modelcontextprotocol.io) server that parses your source tree into a typed graph of modules, functions, types, tests, dependencies, contracts, and tech debt. It exposes that graph to any MCP-compatible AI client (Claude Code, Cursor, Codex, Claude Desktop) as structural-query tools. The graph lives in an embedded local store on your machine, persists across sessions, and is incrementally maintained via a file watcher. ## Quickstart ```bash npx @cognisos/fabric-mcp ``` Your AI client launches the server for you. See the integration pages for client-specific config: - [Claude Code](/docs/integrations/claude-code) - [Cursor](/docs/integrations/cursor) Once configured, ask your client to run `fabric_index` on your project, then `fabric_status`. ## What it indexes 13 languages with tree-sitter parsing. Modules, functions, types, interfaces, tests, and dependencies as graph nodes; structural edges (`Contains`, `Imports`, `Tests`, `Implements`, `Extends`, `Exports`) between them. - **Full call-graph resolution** — TypeScript, Python, Go, Rust, Java - **Structural extraction** — C, C++, C#, Ruby, PHP, Swift, Kotlin, Scala - **Regex fallback** — Shell, Lua, R, Perl, and others `fabric_index` respects `.gitignore` and `.fabricignore`. For the full tool surface, see the [MCP reference](/docs/mcp). ## Performance Median **73 K LoC/sec** with **< 70 MB peak RSS**, measured cold-cache against production OSS repos on M-series macOS. ## Status Public beta. Pin to a version tag (`@cognisos/fabric-mcp@`) for production use. See the [changelog](/docs/changelog/fabric-mcp). ## License MIT. --- # /docs/mcp # @cognisos/fabric-mcp Fabric MCP server for Claude Code, Cursor, Windsurf, and any other client that speaks the Model Context Protocol. Builds a local knowledge graph of your codebase and exposes it as tools. ## Tools - **Code intelligence** (9 tools): `fabric_index`, `fabric_reindex`, `fabric_query`, `fabric_slice`, `fabric_impact`, `fabric_contracts`, `fabric_staleness`, `fabric_recent_changes`, `fabric_explain` — tree-sitter-parsed graph over your codebase. - **Governance & context** (4 tools): `fabric_debt`, `fabric_context_gen`, `fabric_govern`, `fabric_govern_add` — team rules, tech-debt tracking, auto-generated `CLAUDE.md` / `.cursorrules` / `copilot-instructions.md`. - **Cloud sync** (3 tools): `fabric_sync_now`, `fabric_list_snapshots`, `fabric_pull` — knowledge graph backup, listing, and cross-device restore. - **Status** (1 tool): `fabric_status` — indexer health + per-type node/edge counts. ## Install (one command) ```bash npx -y @cognisos/fabric-mcp setup ``` That command opens your browser, signs you in at cognisos.ai, stores an API key in your OS keychain, and registers Fabric as a local stdio MCP in Claude Code. Restart Claude Code, type `/mcp`, and the tools are available. ## How it works - **Indexer tools** spawn a local Rust binary that reads your code, parses it with tree-sitter, and builds a local knowledge graph at `~/.fabric/index`. Your code never leaves your machine. - **Cloud sync** uploads snapshots of your local store (graph only, no source) to your Fabric account for disaster recovery and cross-device restore. - **API key** is stored in your OS keychain (macOS Keychain, GNOME Keyring, Windows Credential Manager) via `@napi-rs/keyring`. Never written to disk in plaintext. ## Platforms | Platform | Supported | |---|---| | macOS Apple Silicon (darwin-arm64) | ✅ | | macOS Intel (darwin-x64) | ✅ | | Linux x64 | ✅ | | Linux ARM64 | ✅ | | Windows x64 | ✅ | ## Subcommands ```bash npx @cognisos/fabric-mcp # run the MCP server in stdio mode (what Claude Code spawns) npx @cognisos/fabric-mcp setup # one-time install: OAuth + keychain + claude mcp add npx @cognisos/fabric-mcp login # re-auth (opens browser again, updates keychain) npx @cognisos/fabric-mcp logout # wipe keychain entry npx @cognisos/fabric-mcp doctor # environment health check ``` ## Manual install (for CI / headless) If `setup` can't open a browser (SSH, tmux, container): ```bash # 1. Get an API key from https://cognisos.ai/dashboard → Developer # 2. Register the MCP with the key passed as an env var claude mcp add fabric -s user \ -e FABRIC_API_KEY=lim_xxxxxxxxxxxxx \ -- npx -y @cognisos/fabric-mcp ``` ## License Proprietary — © Cognisos, Inc. --- # /docs/mcp/tools ## MCP Tool Reference Fabric exposes the following tools over the Model Context Protocol (MCP). Each tool page includes the full description, input schema, and required fields. | Tool | Description | | ---- | ----------- | | [fabric_context_gen](/docs/mcp/tools/fabric_context_gen) | Auto-generate context files from the indexed knowledge graph. | | [fabric_contracts](/docs/mcp/tools/fabric_contracts) | List every exported symbol in the indexed graph, ranked by inbound fan-in (CALLS edge count). | | [fabric_debt](/docs/mcp/tools/fabric_debt) | Inventory TODO/FIXME/HACK/@deprecated markers with graph context — each debt node surfaces alongside its direct incoming edges so you can see what calls or tests the debt-bearing code. | | [fabric_explain](/docs/mcp/tools/fabric_explain) | Full structural summary of a single symbol: name, node type, file, callers, callees, dependencies, tests covering it, API contracts it satisfies, governance rules that apply, and tech debt it carries. | | [fabric_govern](/docs/mcp/tools/fabric_govern) | Check proposed code against governance rules. | | [fabric_govern_add](/docs/mcp/tools/fabric_govern_add) | Add a governance rule. | | [fabric_impact](/docs/mcp/tools/fabric_impact) | Call before refactoring to see "what breaks if I change this?" Reverse-walks call, import, test, and contract edges to surface every node that would be affected — with a risk label (LOW/MEDIUM/HIGH), the specific API contracts that would break, and tests that may go stale. | | [fabric_index](/docs/mcp/tools/fabric_index) | Index a codebase directory. | | [fabric_list_snapshots](/docs/mcp/tools/fabric_list_snapshots) | List cloud snapshots for a repo. | | [fabric_pull](/docs/mcp/tools/fabric_pull) | Restore a snapshot from the Fabric cloud into the local index. | | [fabric_query](/docs/mcp/tools/fabric_query) | Ask "what symbols match this name and what do they connect to?" Matches nodes by name (case-insensitive substring + optional Damerau–Levenshtein fuzzy), then walks edges up to `depth` hops. | | [fabric_recent_changes](/docs/mcp/tools/fabric_recent_changes) | Files sorted by recency — discover what changed most recently without enumerating paths first. | | [fabric_reindex](/docs/mcp/tools/fabric_reindex) | Call after editing files to keep the graph current without reparsing the whole repo. | | [fabric_slice](/docs/mcp/tools/fabric_slice) | Build an LLM-ready context window for a target symbol, bounded by a real token budget (cl100k_base BPE, the same tokenizer GPT-4 and Claude use). | | [fabric_staleness](/docs/mcp/tools/fabric_staleness) | Does the indexed state match disk? | | [fabric_status](/docs/mcp/tools/fabric_status) | Call this first to orient on the local code index: node/edge counts, governance rule count, store size, and per-type breakdown. | | [fabric_sync_now](/docs/mcp/tools/fabric_sync_now) | Upload a snapshot of the local knowledge graph to the Fabric cloud for disaster recovery. | | [fabric_text_search](/docs/mcp/tools/fabric_text_search) | Graph-aware regex grep. | | [fabric_visualize](/docs/mcp/tools/fabric_visualize) | Open an interactive 3D code graph of the indexed codebase in your default browser. | --- # /docs/integrations/claude-code # Claude Code Add Fabric to [Claude Code](https://claude.com/claude-code) so the agent has persistent memory and code-graph tools across sessions. ## Prerequisites Node.js 20+, Claude Code 2.x, and a repo you want indexed. ## Install ```bash npx -y @cognisos/fabric-mcp setup ``` The setup wizard registers Fabric in `~/.claude/mcp_settings.json`. ## Verify Restart Claude Code, then run `/mcp` — you should see `fabric` listed as **connected**. ## Try it ```text > Index this repo with fabric_index, then use fabric_explain to tell me what calls `parseDoc`. ``` Claude answers from the local graph — no source upload. ## See also - [Cursor](/docs/integrations/cursor) — same Fabric, different host --- # /docs/integrations/cursor # Cursor Add Fabric MCP to [Cursor](https://cursor.com) so its agent shares the same code-graph and memory layer as Claude Code. ## Prerequisites Node.js 20+, Cursor 0.43+, and a repo you want indexed. ## Configure Add Fabric to `~/.cursor/mcp.json` (global) or `/.cursor/mcp.json` (per project): ```json { "mcpServers": { "fabric": { "command": "npx", "args": ["-y", "@cognisos/fabric-mcp"], "env": { "FABRIC_REPO_ROOT": "${workspaceFolder}" } } } } ``` ## Verify Restart Cursor. Open chat, click the tool icon, and confirm `fabric.*` tools are listed. ```text > @fabric find every reference to `useDocsToc` ``` ## See also - [Claude Code](/docs/integrations/claude-code) — same install, different host --- # /docs/connectors/github # GitHub Connector **Two-way sync between a GitHub installation and the Fabric graph — PRs, issues, comments, reviews, and push events. Posts an auto-managed PR-summary comment and answers `/fabric` slash commands using the fabric-indexer code knowledge graph.** ## What it does - **Ingests** pull requests, issues, issue/PR-review comments, review state, and pushes via GitHub App webhooks. - **Auto-comments** on every opened/reopened/synchronized PR with a code-intel summary anchored by the sentinel ``. Subsequent commits edit that comment in place rather than posting duplicates. - **Answers slash commands** posted as PR/issue comments — `/fabric explain `, `/fabric impact `, `/fabric slice `, `/fabric query `, `/fabric help`. - **Backfills** historical PR diffs page-by-page on first install. The cursor is bounded under PG's 100 KB row ceiling and resumable across worker restarts. - **Emits back** issue/PR comments, review comments, labels, and commit statuses via `Connector::emit` — every outbound write is recorded in `github_emitted_events` so GitHub's redelivery can be deduped. Unlike Slack, one install is bound to a single **`(installation_id, tenant_id)`** pair: a single GitHub org can be installed into multiple Fabric tenants without conflict because GitHub issues a distinct installation per (App, account) pair. ## Prerequisites - GitHub org/user admin who can install Apps on the target org. - Public-internet-reachable deployment URL for GitHub's webhook POSTs. v1 deliberately does **not** ship a public install callback — operators run the admin endpoint below after the GitHub App install lands. - `FABRIC_MASTER_KEY` set on the daemon — the installation token is encrypted at rest with this key. - `FABRIC_ADMIN_ENDPOINTS=1` and `FABRIC_ADMIN_API_TOKEN=` set on the daemon — required by the admin install endpoint that provisions each tenant's install row (see [Provisioning the install row](#provisioning-the-install-row-v1) below). Without these the route returns 404 / 500 respectively. - For the indexer-driven PR-summary / slash-command paths: one `fabric-mcp-server` Railway service per `(install_id, repo_id)`, reached over HTTP JSON-RPC. The connector resolves daemon URLs by substituting `{install}` / `{repo}` into `FABRIC_INDEXER_DAEMON_URL_TEMPLATE`. ## Create the GitHub App 1. **Settings → Developer settings → GitHub Apps → New GitHub App** (or use the manifest-flow at ). 2. Paste the contents of [`github-app-manifest.json`](https://github.com/cognisos-ai/Prod_Fabric/blob/main/github-app-manifest.json) — replace `your-host.example.com` with your deployment hostname. 3. After creation GitHub gives you three secrets to wire into the daemon: - **App ID** → `GITHUB_APP_ID` - **Private key** (download the `.pem`) → `GITHUB_APP_PRIVATE_KEY_PEM` (the full PEM body, including `-----BEGIN`/`-----END` lines) - **Webhook secret** (you set this) → `GITHUB_WEBHOOK_SECRET` 4. **Install** the App: visit `https://github.com/apps//installations/new` and pick the org + repos. ### Provisioning the install row (v1) v1 of the connector does **not** ship a public install-callback HTTP route, and the manifest above deliberately does **not** advertise `redirect_url`/`callback_urls`. Instead, an authenticated **admin endpoint** provisions the install. The endpoint is the only correct way to seed an install in v1 — raw SQL cannot work because `connector_credentials.credentials_enc` is encrypted per-tenant off `FABRIC_MASTER_KEY`, so the resolver fails to decrypt anything written outside the runtime crypto path. (Without a credentials row, every webhook + worker call later fails to mint an installation token.) > **For UI integrators:** the GitHub manifest exposes its install endpoint via `install_path` in the `/api/v1/connectors` listing. Clients walking that listing should prefer `install_path` (when present) over the `auth_mode`-implied default (`/oauth/start` for OAuth2). For `github` the value is `/api/v1/connectors/github/admin/install`; the framework also refuses `/oauth/start` for connectors that declare a custom install path, naming the correct endpoint in the error body. After installing the App on GitHub (step 4 above), record the **installation id**, **account login**, **account type**, and **repository selection** from the GitHub UI, then call: ```bash curl -X POST "$FABRIC_API/api/v1/connectors/github/admin/install" \ -H "Authorization: Bearer $FABRIC_API_KEY" \ -H "X-Fabric-Admin-Token: $FABRIC_ADMIN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "user_id": "", "github_installation_id": 12345678, "account_type": "Organization", "account_login": "acme", "repository_selection": "all" }' ``` The endpoint: - Validates the App can mint an installation token for the supplied id (proves the install actually belongs to this App, not a sibling deployment). - Writes `connector_installs`, `github_app_installations`, and `connector_credentials` in one transaction — credentials are routed through `CredentialStore::finalize_install_in_tx` so the AEAD encryption matches what the resolver expects. - Returns the newly-minted `install_id`. The route requires **two factors** before any tenant work happens: - `FABRIC_ADMIN_ENDPOINTS=1` on the deployment — when unset, the route returns 404 (endpoint invisible). - A deploy-admin shared secret in `X-Fabric-Admin-Token`, constant-time compared against the `FABRIC_ADMIN_API_TOKEN` env var. The tenant API key still authenticates the caller (and is the only thing that binds the install to a specific `tenant_id`), but the admin token gates *who* can call the endpoint at all — without it, a regular tenant API key cannot pre-claim a GitHub installation id meant for a different tenant. Missing/wrong header → 401; `FABRIC_ADMIN_API_TOKEN` unset while the endpoint is enabled → 500 with an explicit "admin auth not configured" message so the misconfig is loud. Wiring a real public install handoff (state-token-bound tenant binding via `OAuthStateStore`) is tracked as a follow-up — at that point the admin token + endpoint env gate retire. The connector is **gated behind `GITHUB_CONNECTOR_ENABLED=true`** — the webhook route returns 501 until all five envs (`GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY_PEM`, `GITHUB_WEBHOOK_SECRET`, `FABRIC_INDEXER_DAEMON_URL_TEMPLATE`, and `FABRIC_MASTER_KEY`) are set. ## Environment ```bash export GITHUB_CONNECTOR_ENABLED=true export FABRIC_MASTER_KEY= export GITHUB_APP_ID=123456 export GITHUB_APP_PRIVATE_KEY_PEM="-----BEGIN RSA PRIVATE KEY----- … -----END RSA PRIVATE KEY-----" export GITHUB_WEBHOOK_SECRET= # Used to format per-(install, repo) indexer URLs. {install} and {repo} # MUST land in separate DNS labels (or in path segments) — the 36-char # install UUID plus any reasonable repo slug would overshoot the 63-char # DNS-label limit if collapsed into a single label, and format_daemon_url # rejects that at the boundary. export FABRIC_INDEXER_DAEMON_URL_TEMPLATE=https://fabric-indexer.{install}.{repo}.up.railway.app # Path-segment placement bypasses DNS label limits entirely if your daemon # routes by URL path rather than by hostname: # FABRIC_INDEXER_DAEMON_URL_TEMPLATE=http://fabric-indexer/install/{install}/repo/{repo} ``` ## Slash commands Post any of these as a comment on an issue or PR. The bot replies under the sentinel `` so its own replies are filtered from re-triggering. | Verb | Args | What it does | | --- | --- | --- | | `/fabric explain` | `` | Calls `fabric_explain` against the per-repo indexer daemon and replies with the explanation. | | `/fabric impact` | `` | Calls `fabric_impact` — surfaces call sites + reverse-deps of the symbol. | | `/fabric slice` | `` | Calls `fabric_slice` for a token-bounded code slice anchored at the path. | | `/fabric query` | `` | Free-form query against the fabric MCP. | | `/fabric help` | _(none)_ | Renders the static help message inline. | All replies are capped at 60 000 chars (UTF-8 char-wise truncation) so even runaway tool output never breaks GitHub's comment-size limit. ## Backfill First install kicks off a three-phase walk over the installation's repos: 1. **ReposListing** — paginate `/installation/repositories?per_page=100`. 2. **PrEnumerating** — paginate `/repos/{owner}/{repo}/pulls?state=all&per_page=100` for each repo. 3. **PrDiffFetching** — `GET /repos/{owner}/{repo}/pulls/{n}` with `Accept: application/vnd.github.v3.diff` per PR. Each `Connector::backfill` invocation makes **exactly one** GitHub API call so the framework's per-page rate gate charges `github_general` once per page. The cursor is bounded under PG's 100 KB row ceiling via `PENDING_REPOS_CAP=1000` + `PENDING_PRS_CAP=10000`; when those queues fill, the FSM spills back to `ReposListing` with a `repos_resume_page` marker so a worker crash mid-walk resumes from the last committed cursor on the next claim. Oversized PRs (GitHub returns HTTP 406 on the diff endpoint) yield events with `oversized: true` and no `diff` field — downstream can choose to skip or summarize them rather than failing the page. Backfill events use namespaced kind `pull_request.backfill_diff` and id `backfill:pr-diff:{repo_id}:{pr_number}` so they never collide with live `pull_request.opened` webhooks on dedup. ## Emit `Connector::emit` accepts five `payload.kind` values. Each is validated synchronously at the boundary so a bad payload fails with `BadConfig` rather than a confusing GitHub 422. | Kind | Body fields | Notes | | --- | --- | --- | | `github.pr_summary_comment` | `repo_full_name`, `pr_number`, `body`, optional `existing_comment_id` | Body MUST contain the `` sentinel; `existing_comment_id` triggers PATCH (edit-in-place), absence triggers POST. | | `github.issue_comment` | `repo_full_name`, `issue_number`, `body` | Free-form. | | `github.review_comment` | `repo_full_name`, `pr_number`, `commit_id`, `path`, `line`, `body`, optional `side` (`LEFT`/`RIGHT`) | Line-anchored. | | `github.label` | `repo_full_name`, `issue_number`, `labels` (non-empty array) | Whitespace-only entries are stripped; if the result is empty, returns `BadConfig`. | | `github.commit_status` | `repo_full_name`, `sha`, `state` (`error`/`failure`/`pending`/`success`), optional `target_url`/`description`/`context` | Description must be ≤140 chars (GitHub's limit). | Every successful call writes a row to `github_emitted_events` keyed on `(install_id, kind, repo_full_name, issue_or_pr_number, provider_id)`. Failures to record are logged but never rolled back — the GitHub call is the source of truth. ## Self-emit dedup Two filters keep the connector from re-ingesting its own writes when GitHub redelivers a webhook: - **Tier-1** (in-memory, infallible): drops `issue_comment` events with `sender.type == "Bot"`, and drops any comment whose body contains `` or ``. - **Tier-2** (single SELECT against `github_emitted_events`): drops events whose `comment.id` matches a row this install wrote. Catches edge cases where GitHub re-attributes the comment to a `User` sender, or an upstream mirror tool strips the sentinel. Tier-2 is best-effort: a DB failure logs and proceeds with Tier-1 survivors. Tier-1 alone is sufficient under normal operation. ## Coordination with the Notion connector The Notion skill imports GitHub issues as Notion Tasks. To avoid double-ingestion, **v1 of the GitHub connector ingests issues/comments into Fabric but does NOT auto-create Notion Tasks** — the Notion skill remains the only path that creates them. Provenance on every artifact carries `connector_id = "github"` so cross-connector queries can disambiguate. ## Tables - `github_app_installations` — `(install_id, github_installation_id, tenant_id, account_login, account_type, repository_selection, app_id)`. RLS deny-all for `app_role`; read-only via service role (same shape as `slack_workspace_installs`). - `github_emitted_events` — `(install_id, kind, repo_full_name, issue_or_pr_number, provider_id, by_actor_login, emitted_at)`. Composite PK doubles as the Tier-2 dedup index. - `github_repo_index_state` — `(install_id, repo_id, daemon_url, status, last_indexed_sha, …)`. Tracks per-repo clone + index lifecycle. ## Conflicts A second tenant trying to install the same GitHub org gets its OWN install row — GitHub issues a fresh `installation_id` per App-account binding so no conflict exists. The exception is when an admin uses the same `(App, account)` pair across two deployments: that's a configuration error (one App per deployment), not a tenant conflict. ## Health The framework-wide install health endpoint applies — `GET /api/v1/connectors/installs/:install_id/health` (registered in `routes/connectors.rs`, identical shape for every connector). It dispatches to `GithubConnector::health` which returns: - Step 1 / disabled: `{ "status": "skeleton" }`. - Wired and reachable: round-trips `GET /app` + `GET /installation/{id}` and surfaces `{ "status": "healthy", "github_installation_id": }`. - Token-mint failure: 503 + the upstream message. ## Limits - **One Railway service per `(install_id, repo_id)`** for the indexer daemon. Cap repo count per tenant in v1; multi-repo-per-daemon is a follow-up. - **PR diff size**: 1 MB cap; oversized diffs are flagged in the backfill event. - **Initial index** of a 500 k-file repo can exceed Railway's default service timeout. The worker uses `defer_job` to extend lock past the 5-min default. - **GitHub App private key** is read from env as a PEM string. Multi-region / per-tenant App keys are not supported in v1. --- # /docs/connectors/obsidian # Obsidian Connector **Two-way sync between an Obsidian vault and the Fabric graph — local, file-watcher driven, no plugin install.** ## What it does Points the self-hosted Fabric daemon at one or more Obsidian vault folders. The connector: - **Ingests** every `.md` note as a Fabric node — YAML frontmatter, body, `#tags`, and `[[wikilinks]]` extracted automatically. - **Stays current** via a filesystem watcher (changes flow into Fabric within ~200 ms), with a 60 s interval poll as a fallback. - **Emits back** when Fabric writes a note — atomic tempfile + rename, conflict-safe sidecar on concurrent external edits. - **Tracks renames** by content-derived stable IDs (frontmatter `id:` if present, else SHA-256 of body) so wikilink targets survive moves. No Obsidian plugin, no auth flow. The daemon reads the vault directory directly. ## Prerequisites - Self-hosted Fabric daemon (`rsc-api-server`) running on the same host as the vault — local-host only in v1. - Obsidian vault is a plain directory of `.md` files (no encrypted/iCloud-locked vaults). - POSIX filesystem for atomic emit. macOS and Linux are first-class; Windows is best-effort (non-atomic rename across volumes). ## Configure Set `OBSIDIAN_VAULTS` before starting the daemon: ```bash export OBSIDIAN_VAULTS="research=/Users/me/Obsidian/Research,journal=/Users/me/Obsidian/Journal" ``` The value is a comma-separated list of `vault_id=path` pairs. The `vault_id` is a stable handle you choose — it appears in install IDs and the `provenance.source_id` of every ingested record. Optional: | Variable | Default | Purpose | | -------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------- | | `OBSIDIAN_TENANT_ID` | `0b51d1a4-0b51-4d1a-b51d-1a40b51d1a40` | Tenant UUID for ingested records. Override when running multi-tenant. | | `OBSIDIAN_USER_ID` | (per-vault derived) | Pin every install to one user profile. **Single-vault only** — rejects 2+ vaults. | Bad UUIDs in either variable fail boot with a clear error. Bad paths in `OBSIDIAN_VAULTS` skip just that install and log a warning; other vaults still register. ## Verify Start the daemon and check the boot log for one line per vault: ```text INFO Obsidian per-install scheduler started (watcher + interval) install_id=… vault_id=research ``` Then drop a note in the vault — it should appear in the graph within a few seconds. Tags become tag edges, wikilinks become link edges, frontmatter fields land on the node. ## What gets ingested For each `.md` file: - **Frontmatter** — full YAML mapping kept on the node. Top-level sequences or scalars are rejected (the daemon logs and dead-letters the note rather than silently dropping the frontmatter). - **Tags** — both YAML `tags:` and inline `#hashtag` syntax. Tags inside fenced ``` ``` ``` or inline `` `code` `` are ignored. - **Wikilinks** — `[[Target]]` and `[[Target|alias]]` become edges. Pipe-aliases are stripped; the target is canonical. Wikilinks inside code blocks are ignored. - **Body** — the markdown after the frontmatter, verbatim. Hidden directories (`.obsidian/`, `.git/`, `.trash/`) are skipped. ## Emit behavior When Fabric writes a note back to the vault: - The write goes to a tempfile in the same directory, then a `rename(2)` swaps it onto the canonical path — readers never see a partial file. - If the canonical file's mtime has advanced past Fabric's last-seen value (something else changed it), the emit writes to `.conflict--.md` instead. **No clobber.** Operators can grep `tracing` warns for `emit_conflict` to find them. - A `rel_path` containing `..` or absolute path is rejected up front. Dead-letter persistence for emit conflicts is deferred to v1.5 — the sidecar file IS the recovery surface in v1. ## Replay a backfill If the cursor gets out of sync (e.g. you restored the vault from backup), drive a fresh backfill without restarting the daemon: ```bash curl -X POST "http://localhost:3000/api/v1/connectors/installs//backfill" ``` The endpoint refuses if the install isn't `active`. ## Limitations - **Local-host only.** Vault and daemon share a filesystem. Multi-tenant SaaS needs a per-tenant agent (deferred — see [issue #233](https://github.com/cognisos-ai/Prod_Fabric/issues/233)). - **Last-writer-wins** on the canonical path; concurrent writes during the brief read-mtime → rename window can be overwritten silently. Acceptable for a single-user local vault. - **No `PATCH /config`** for Obsidian installs — vault path is fixed at boot. Change `OBSIDIAN_VAULTS` and restart. ## See also - [Fabric Indexer](/docs/fabric-indexer) — the graph engine that stores ingested notes - [MCP Reference](/docs/mcp) — query the indexed vault from your AI client --- # /docs/connectors/slack # Slack Connector **Two-way sync between a Slack workspace and the Fabric graph — channel/DM messages, threads, and reactions. Emits `chat.postMessage` and `reactions.add` back.** ## What it does - **Ingests** every message + reaction in channels the bot is invited to, plus DMs and group DMs. - **Backfills** the workspace history via `conversations.list` → `history` → `replies`, page by page. - **Emits back** when Fabric posts a message or adds a reaction — atomic dedup on echo via a two-tier filter so the bot's own writes never feed back into the graph. - **Survives reinstall** — uninstall is transactional: workspace claim, credentials, status all flip atomically; reinstalling the same workspace afterward just works. One install per Slack workspace **across the entire deployment** — Slack's single-app identity model means two tenants installing the same workspace would share bot identity at Slack's side, so we refuse it. See [Conflicts](#conflicts) below. ## Prerequisites - Slack admin who can install apps in the target workspace. - A reachable deployment URL for OAuth — during install, Slack redirects the installer's browser to your `redirect_uri` callback, so the deployment must be reachable from the installer (typically a public URL). - For **HTTP transport**: Slack must also be able to POST Events API payloads to your `/api/v1/connectors/slack/events` endpoint — inbound from Slack's network. - For **Socket Mode**: no inbound traffic needed. The daemon opens an outbound WSS connection to Slack; OAuth is still subject to the redirect rule above. - `FABRIC_MASTER_KEY` set on the daemon — the Slack bot token is encrypted at rest with this key. ## Create the Slack app 1. Go to → **Create New App** → **From a manifest**. 2. Paste the contents of [`slack-app-manifest.yml`](https://github.com/cognisos-ai/Prod_Fabric/blob/main/slack-app-manifest.yml) from this repo (replace `your-host` with your deployment hostname). 3. **Save** → Slack provisions the app. Visit the app's **Basic Information** page and copy: - **Signing Secret** → daemon env `SLACK_SIGNING_SECRET` (HTTP transport). - **App-Level Token** (generate one with `connections:write`) → daemon env `SLACK_APP_TOKEN` (Socket Mode). ## Pick a transport The connector supports **two** mutually-exclusive transports — pick one per deployment: ### HTTP Events API (default) Slack POSTs events to your deployment over the public internet. Easier to debug (events flow through your normal HTTP stack) and matches how production SaaS apps usually deploy. ```bash export SLACK_CLIENT_ID=… # from app's "Basic Information" export SLACK_CLIENT_SECRET=… export SLACK_SIGNING_SECRET=… export SLACK_APP_TIER=non_marketplace # or "marketplace" if approved # SLACK_USE_SOCKET_MODE unset / false ``` ### Socket Mode Daemon opens an outbound WSS connection to Slack — no inbound HTTP needed, useful behind a NAT/firewall. ```bash export SLACK_CLIENT_ID=… export SLACK_CLIENT_SECRET=… export SLACK_APP_TOKEN=xapp-… # from app's "App-Level Tokens" export SLACK_APP_TIER=non_marketplace export SLACK_USE_SOCKET_MODE=true # SLACK_SIGNING_SECRET unset — HTTP events route won't be mounted ``` Also flip **Settings → Socket Mode → Enable** in the Slack app config. Slack enforces mutual exclusion at its side too — having both transports armed would either double-deliver events or silently break depending on the app config order. The daemon reconnects with exponential backoff (1s → 60s, full jitter) if the WSS connection drops. ## Install into a workspace Once env is set and the daemon is running, walk the OAuth flow per tenant: ```bash # 1. Mint an install URL — the response carries `authorize_url`. curl -sX POST \ "http://localhost:3000/api/v1/connectors/slack/oauth/start" \ -H "Authorization: Bearer $FABRIC_API_KEY" \ -d '{"redirect_uri": "https://your-host.example.com/api/v1/connectors/oauth/callback"}' # 2. Open the returned authorize_url in a browser; Slack walks you through # install + scope consent and redirects back to the callback URL above. # The callback finalizes the install and writes credentials. # 3. Verify the install: curl -s "http://localhost:3000/api/v1/connectors/installs?connector_id=slack" \ -H "Authorization: Bearer $FABRIC_API_KEY" ``` The connector then: - Encrypts the bot token under `FABRIC_MASTER_KEY` and stores it in `connector_credentials`. - Inserts a `slack_workspace_installs` row claiming the workspace globally. - Starts the backfill orchestrator (paginated `conversations.list` → `history` → `replies`). - Begins receiving live events on whichever transport is active. ### Conflicts A second install of the **same** Slack workspace — even from a different tenant — fails with HTTP **409** at the OAuth callback. The Slack-side bot identity is shared across that one app, so two tenants would step on each other; the conflict is enforced atomically by the `slack_workspace_installs.team_id` primary key. ### Reinstall after uninstall Uninstall (`DELETE /api/v1/connectors/installs/`) is transactional: the workspace claim row, credentials revocation, and install status flip happen in one Postgres transaction. After uninstall, the next install of the same workspace **just works** — no 409, no manual cleanup. (Pre-v1 this required operator intervention because the `slack_workspace_installs` row was left orphaned.) ## Emit a message ```bash curl -sX POST \ "http://localhost:3000/api/v1/connectors/installs//emit" \ -H "Authorization: Bearer $FABRIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "slack.message", "body": { "channel_id": "C0123456789", "text": "Hello from Fabric" } }' ``` Response carries `EmitReceipt.provider_id` = Slack's `ts` (use it to reply in-thread later). Emitted messages are **not** ingested back into the graph — the two-tier self-emit filter drops them on the inbound side. ### Rate limits Slack throttles `chat.postMessage` at ~1 msg/sec **per channel** on top of the workspace-wide write budget. The emit route charges both buckets atomically — a throttled per-channel post returns **429** with a `Retry-After` header **without** consuming a workspace-wide token. Burst-posting to unrelated channels still works at the workspace cap (~100/min). ### React to a message ```bash curl -sX POST \ "http://localhost:3000/api/v1/connectors/installs//emit" \ -H "Authorization: Bearer $FABRIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "slack.reaction", "body": { "channel_id": "C0123456789", "timestamp": "1736000000.000001", "name": "thumbsup" } }' ``` ## Uninstall ```bash curl -X DELETE \ "http://localhost:3000/api/v1/connectors/installs/" \ -H "Authorization: Bearer $FABRIC_API_KEY" ``` Effects: - Slack token revoked at Slack (`auth.revoke`). - Local credentials marked revoked, install status flipped to `uninstalled`, workspace claim DELETEd — all in one transaction. - Backfill worker stops ingesting (it gates on `status == "active"`). If the Slack `auth.revoke` call succeeds but the local transaction then fails, the Slack-side token is dead while the install row stays `active`. Retrying uninstall converges — `auth.revoke` is idempotent at Slack's end and the local tx either commits cleanly or surfaces the same error for ops to investigate. ## Limitations - **One install per workspace per deployment.** Cross-tenant workspace sharing is rejected with 409 — see [Conflicts](#conflicts). - **Files are referenced, not downloaded.** A message with attachments lands in Fabric with `payload.files = [{ file_id, name, mimetype, size, permalink }]` — the connector does not fetch file bytes in v1. - **DM coverage requires scope.** If `mpim:history` is missing from the install consent, MPIMs silently drop out of backfill; the connector logs the missing scope at install time. ## See also - [All connectors](/docs#connectors) — other available connector integrations. - [`slack-app-manifest.yml`](https://github.com/cognisos-ai/Prod_Fabric/blob/main/slack-app-manifest.yml) — Slack app manifest to paste into the App Directory. - [Issue #231](https://github.com/cognisos-ai/Prod_Fabric/issues/231) — the design issue for this connector. --- # /docs/changelog/fabric-mcp # fabric-mcp Changelog All notable changes to `@cognisos/fabric-mcp` are documented here. ## Unreleased - Initial dedicated changelog file. Pre-release history is recorded in the project root `CHANGELOG.md` and in git history of `packages/fabric-mcp`. ## 0.2.0-rc.22 - Latest release-candidate. Full per-rc notes live in the root [Cognisos Changelog](../../CHANGELOG.md). For per-commit detail run: ```bash git log --oneline -- packages/fabric-mcp ```