From 849dbcd929d6d454400c73a68b62ee3a56522e70 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Sat, 15 Aug 2026 17:18:35 -0400 Subject: [PATCH 1/5] =?UTF-8?q?ci:=20cli-contract=20check=20is=20real=20no?= =?UTF-8?q?w=20=E2=80=94=20manifest=20fetch=20failures=20are=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fluree/db v4.1.5 publishes fluree-cli-manifest.json as a release asset, so the 404-warn-and-skip branch this job carried since #1 has lost its reason to exist. Any fetch failure is now an infrastructure failure and the contract check always runs. cli-facts.json validates clean against the live 4.1.5 manifest (42 commands checked). Closes #2. --- .github/workflows/ci.yml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cce1542..c9e63ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,23 +54,14 @@ jobs: # `fluree manifest` command) and published as a release asset by # fluree/db — validating against it means validating against what # actually shipped, without building anything. The URL lives in the - # facts file (single source of truth). + # facts file (single source of truth). fluree/db v4.1.5 ships the + # asset, so a missing manifest is an infrastructure failure now, not + # a skip (issue #2). - name: Download latest fluree CLI manifest - id: manifest run: | set -euo pipefail url=$(node -p "JSON.parse(require('node:fs').readFileSync('plugins/fluree-cli/contract/cli-facts.json','utf-8')).manifest_source") - code=$(curl -sSL -w '%{http_code}' -o /tmp/fluree-cli-manifest.json "$url" || echo 000) - if [ "$code" = "200" ]; then - echo "found=true" >> "$GITHUB_OUTPUT" - elif [ "$code" = "404" ]; then - echo "found=false" >> "$GITHUB_OUTPUT" - echo "::warning::fluree-cli-manifest.json is not on the latest fluree/db release yet — contract check SKIPPED (tracked to be made required in issue #2). Remove this branch once fluree/db#1571 ships in a release." - else - echo "::error::manifest download failed with HTTP $code — an infrastructure failure, not a missing-asset skip." - exit 1 - fi + curl -fsSL -o /tmp/fluree-cli-manifest.json "$url" - name: Check cli-facts contract - if: steps.manifest.outputs.found == 'true' run: node scripts/check-cli-contract.mjs plugins/fluree-cli/contract/cli-facts.json /tmp/fluree-cli-manifest.json From b88a547891670539020e053a06bbe3e5ee4e886f Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Sat, 15 Aug 2026 17:46:52 -0400 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20fluree-companion=20plugin=20?= =?UTF-8?q?=E2=80=94=20stack-first=20companion=20with=20a=20manifest-check?= =?UTF-8?q?ed=20stack=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second application of the fluree-cli plugin's pattern, aimed at the deployment rather than the binary: probe the stack, don't recall the platform. The skill carries connection choreography, the CLI/Space-MCP/UI division of labor, safety rails, and per-stack version awareness; the playbooks route to stack doc slugs instead of restating procedure that would drift the moment a stack upgrades. The stack's docs MCP tools are treated as probe-then-use throughout — they are still landing solo-side, and a stack older than that release won't have them, so every knowledge path falls back to fetching the public /api/docs index over HTTP. contract/stack-facts.json enumerates every address the choreography can't avoid naming, and check-stack-facts.mjs falsifies it in three directions: router paths resolve against solo's generated endpoint manifest (including their auth posture, since the whole premise of the discovery probe is that it works pre-auth); UI-Lambda paths must NOT resolve there, which is what catches the ownership split moving; and doc slugs check against a live /api/docs index when STACK_ORIGIN is set, asserting public slugs are present AND in-app slugs are absent. The plugin names fluree commands too, so it carries its own cli-facts.json and the CI job now loops every plugin's — a plugin that names a command is on the hook for it existing, whichever plugin it is. The endpoint manifest doesn't exist on solo's main yet (fluree/solo#1041), so that job warns and skips on 404, the same lifecycle the cli-contract check went through before #2 landed. --- .claude-plugin/marketplace.json | 6 + .github/workflows/ci.yml | 38 ++- .../.claude-plugin/plugin.json | 9 + plugins/fluree-companion/README.md | 40 +++ plugins/fluree-companion/commands/connect.md | 31 +++ .../fluree-companion/contract/cli-facts.json | 21 ++ .../contract/stack-facts.json | 95 +++++++ .../skills/fluree-companion/SKILL.md | 58 ++++ .../fluree-companion/references/connect.md | 76 +++++ .../references/stack-knowledge.md | 49 ++++ .../fluree-companion/references/stories.md | 43 +++ scripts/check-stack-facts.mjs | 261 ++++++++++++++++++ 12 files changed, 725 insertions(+), 2 deletions(-) create mode 100644 plugins/fluree-companion/.claude-plugin/plugin.json create mode 100644 plugins/fluree-companion/README.md create mode 100644 plugins/fluree-companion/commands/connect.md create mode 100644 plugins/fluree-companion/contract/cli-facts.json create mode 100644 plugins/fluree-companion/contract/stack-facts.json create mode 100644 plugins/fluree-companion/skills/fluree-companion/SKILL.md create mode 100644 plugins/fluree-companion/skills/fluree-companion/references/connect.md create mode 100644 plugins/fluree-companion/skills/fluree-companion/references/stack-knowledge.md create mode 100644 plugins/fluree-companion/skills/fluree-companion/references/stories.md create mode 100644 scripts/check-stack-facts.mjs diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 50f7b1c..0e3cd19 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,6 +8,12 @@ "description": "Internal Claude Code plugins for Fluree demos and tooling" }, "plugins": [ + { + "name": "fluree-companion", + "source": "./plugins/fluree-companion", + "description": "Connect Claude Code to your Fluree AI (Solo) stack and work against it — discovery, human-approved device login, Space MCP registration, and playbooks that read the stack's own version-pinned docs. Includes /fluree-companion:connect.", + "version": "1.0.0" + }, { "name": "fluree-dataset-generator", "source": "./plugins/fluree-dataset-generator", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9e63ec..9804d21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,5 +63,39 @@ jobs: url=$(node -p "JSON.parse(require('node:fs').readFileSync('plugins/fluree-cli/contract/cli-facts.json','utf-8')).manifest_source") curl -fsSL -o /tmp/fluree-cli-manifest.json "$url" - - name: Check cli-facts contract - run: node scripts/check-cli-contract.mjs plugins/fluree-cli/contract/cli-facts.json /tmp/fluree-cli-manifest.json + # Every plugin that names a `fluree …` command is on the hook for it + # existing, not just the CLI plugin — so check each cli-facts.json. + - name: Check cli-facts contracts + run: | + set -e + for f in plugins/*/contract/cli-facts.json; do + echo "── $f" + node scripts/check-cli-contract.mjs "$f" /tmp/fluree-cli-manifest.json + done + + stack-contract: + name: fluree-companion contract vs solo endpoint manifest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # The endpoint manifest is generated from the Solo router's own route + # table and committed to fluree/solo. It does not exist on solo's main + # yet (fluree/solo#1041), so a 404 warns and skips — the same lifecycle + # the cli-contract job went through, and it flips to a hard failure + # here the moment that PR merges. + - name: Download solo router endpoint manifest + id: manifest + run: | + set -uo pipefail + url=$(node -p "JSON.parse(require('node:fs').readFileSync('plugins/fluree-companion/contract/stack-facts.json','utf-8')).manifest_source") + if curl -fsSL -o /tmp/endpoint-manifest.json "$url"; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + echo "::warning::endpoint manifest not published yet ($url) — skipping the stack-facts check. Remove this skip when fluree/solo#1041 merges." + fi + + - name: Check stack-facts contract + if: steps.manifest.outputs.available == 'true' + run: node scripts/check-stack-facts.mjs plugins/fluree-companion/contract/stack-facts.json /tmp/endpoint-manifest.json diff --git a/plugins/fluree-companion/.claude-plugin/plugin.json b/plugins/fluree-companion/.claude-plugin/plugin.json new file mode 100644 index 0000000..fddaa46 --- /dev/null +++ b/plugins/fluree-companion/.claude-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "fluree-companion", + "description": "Stack-first companion for Fluree AI (Solo) — connection choreography and playbooks that defer to the stack's own version-pinned knowledge. Includes /fluree-companion:connect.", + "version": "1.0.0", + "author": { + "name": "Fluree Dev Rel" + }, + "homepage": "https://github.com/fluree/claude-plugins/tree/main/plugins/fluree-companion" +} diff --git a/plugins/fluree-companion/README.md b/plugins/fluree-companion/README.md new file mode 100644 index 0000000..9b86d16 --- /dev/null +++ b/plugins/fluree-companion/README.md @@ -0,0 +1,40 @@ +# fluree-companion — Claude Code companion for a Fluree AI stack + +Takes you from a bare **Fluree AI** (Fluree Solo) stack URL to a working session — and then keeps the answers coming from *your* stack, at *your* stack's version, instead of from a model's memory of the platform. + +Every customer runs their own stack at their own release. So this plugin deliberately carries no platform reference: it carries the choreography for connecting, and the discipline of asking the stack. + +## What it does + +- **A `fluree-companion` skill** that auto-triggers on stack work. It carries doctrine, not documentation: + - **The stack is the source of truth.** Probe `/.well-known/fluree.json` for capabilities, auth, and the minimum CLI version; read how-to knowledge from the stack's own docs — its MCP docs tools when present, else the public `/api/docs` index — never from trained recall. + - **Connection choreography.** Discovery → `fluree remote add` → device-code `fluree auth login`, where the *human* approves at the stack's `/activate` page → optionally registering a Space's MCP server. + - **Division of labor.** The CLI for data and admin work on ledgers, a Space's MCP server for querying governed data with the Space's tools, the UI for what only it does — and a rule against working around governance. + - **Safety rails.** `.fluree/` holds live tokens; a `cli.min_version` warning is a stop-and-upgrade signal; destructive operations against shared stack data get named confirmation. + - **Version awareness.** What you learn from one stack is a fact about that stack at that release, and doesn't travel. +- **`/fluree-companion:connect`** — the guided run: binary check, discovery with a min-CLI verdict, remote + human-approved login, `fluree list` verification, optional Space MCP registration, and a knowledge-access check that has to actually read something before reporting success. + +Pairs with **[fluree-cli](../fluree-cli/README.md)**, which covers driving the `fluree` binary itself (flags, local ledgers, imports, policy authoring). Rule of thumb: *this* plugin when the subject is a stack, that one when the subject is the CLI. Both installed is the intended setup. + +## Install + +```text +/plugin marketplace add fluree/claude-plugins +/plugin install fluree-companion@fluree-plugins +``` + +Then run `/fluree-companion:connect`, or just say what you're trying to do with your stack. + +## How this stays in sync with a stack + +Everything version-specific lives on the stack and is fetched at use time. The handful of addresses the choreography can't avoid naming — discovery and OAuth metadata paths, the Space MCP endpoint, the docs index, and the doc slugs the playbooks point at — are enumerated in [`contract/stack-facts.json`](contract/stack-facts.json) and checked in CI by [`scripts/check-stack-facts.mjs`](../../scripts/check-stack-facts.mjs): + +- **Router paths** are resolved against solo's generated endpoint manifest (`src/contracts/generated/router/endpoint-manifest.json`), which is produced from the router's own route table. A path the plugin names that no longer exists is a CI failure here, not a 404 for a user. The check also asserts each path's **auth posture** matches what the prose claims — the pre-auth discovery probe stops being pre-auth the moment that changes. +- **UI-Lambda paths** (`/api/docs`, `/activate`, `/docs/…`) aren't in that manifest by design, since the Next.js UI serves them. What the manifest *can* falsify is the ownership split the prose teaches, so the checker fails if one of them starts resolving in the router. +- **Doc slugs** are checked against a live stack's `/api/docs` index when `STACK_ORIGIN` is set. Slugs marked `public` must be listed there; slugs marked `in-app` must not — that inversion catches the day a page is promoted into the public set and the plugin should be handing out a fetchable URL instead of a signed-in one. +- **Stack MCP tool names** (`docs_search` and friends) are recorded but not validated — they have no published producer artifact yet, which is exactly why the skill probes for them rather than assuming them. + +The choreography also names `fluree` commands, so this plugin carries its own +[`contract/cli-facts.json`](contract/cli-facts.json) — checked against fluree/db's +`fluree-cli-manifest.json` release asset by the same checker the `fluree-cli` plugin uses. A +plugin that names a command is on the hook for it existing, whichever plugin it is. diff --git a/plugins/fluree-companion/commands/connect.md b/plugins/fluree-companion/commands/connect.md new file mode 100644 index 0000000..bbaaf1b --- /dev/null +++ b/plugins/fluree-companion/commands/connect.md @@ -0,0 +1,31 @@ +--- +description: Connect this session to a Fluree AI (Solo) stack — discovery, device-code login, optional Space MCP, and a knowledge-access check +--- + +Take a session from "I have a stack URL" to "I can work against that stack." Follow these steps in order, reporting progress as you go. Stop and ask if a step's verification doesn't pass — don't chain past a failure. + +The choreography and its failure modes are in the `fluree-companion` skill's `references/connect.md`; this command is the guided run of it. + +1. **Binary check**: run `fluree --version`. If it's missing or you need to upgrade, hand that off to **`/fluree-cli:setup`** (the `fluree-cli` plugin, same marketplace) rather than improvising — it also covers the common case where a new binary is shadowed on PATH by an old one. If that plugin isn't installed, point the user at `https://github.com/fluree/db/releases/latest` and stop here. + +2. **Get the stack origin**: ask for it if it wasn't given — just the origin, e.g. `https://my-stack.example.com`, no path. If the user only has a UI URL, the origin is its scheme + host. + +3. **Discover**: fetch `/.well-known/fluree.json`. Report back, in plain terms: + - what the stack advertises (`capabilities`, `import` modes, `api_base_url`), + - the auth type (`oidc_device` → the device flow below; `token` → the user must supply a token), + - and an explicit **min-CLI verdict**: `cli.min_version` vs the version from step 1, stated as OK or "upgrade needed before continuing." + A 404 here means the stack predates the discovery endpoint — you can still connect, but do not infer capabilities you couldn't read. + +4. **Project hygiene**: if this is a git repo where `fluree init` will run, confirm `.gitignore` covers `.fluree/` (it holds live access and refresh tokens after login). Add it if missing. + +5. **Add the remote**: `fluree init` if needed, then `fluree remote add /v1/fluree`. Let the user pick the alias, or propose one from the host. Surface any min-version warning the command prints. + +6. **Log in — the human approves**: run `fluree auth login --remote `. It prints a device code and blocks. **Immediately tell the user to approve it at the stack's `/activate` page in their browser**, and wait for them. Never automate or bypass this. Then verify: `fluree auth status --remote ` and `fluree list --remote `. An empty dataset list with healthy auth is fine — say so rather than treating it as a failure. + +7. **Offer Space MCP registration** (optional — ask, don't assume): if the user wants to query a Space's governed data directly, they need the Space's `spaceId` from its **MCP** tab, where an owner must first have enabled MCP access. Then: + `claude mcp add --transport http fluree-space "https:///v1/mcp?space="` + Claude Code discovers the OAuth configuration from the stack and prompts for sign-in. + +8. **Verify knowledge access**: confirm you can read this stack's own docs, since that's the point of connecting. Probe for the stack's docs MCP tools (`docs_search` and friends) and use one if present; otherwise fetch `/api/docs` and confirm the index parses. Report which of the two you got — do not report success without having actually read something. + +Finish with a one-line status: stack origin, CLI version vs `cli.min_version`, remote alias and auth state, Space MCP registered or not, and the live knowledge path (stack docs tools, or `/api/docs` over HTTP). diff --git a/plugins/fluree-companion/contract/cli-facts.json b/plugins/fluree-companion/contract/cli-facts.json new file mode 100644 index 0000000..a8f4e01 --- /dev/null +++ b/plugins/fluree-companion/contract/cli-facts.json @@ -0,0 +1,21 @@ +{ + "contract_version": 1, + "comment": "The fluree CLI commands this plugin's connection choreography and playbooks name. Validated by scripts/check-cli-contract.mjs against the same fluree-cli-manifest.json release asset as the fluree-cli plugin's facts file — a plugin that names a command is on the hook for it existing, whichever plugin it is. Deliberately smaller than fluree-cli's: flags belong to that plugin's doctrine, and the choreography here names commands, not flag surfaces. Note the floor: `remote add`'s automatic cli.min_version warning needs 4.1.5, which is why /fluree-companion:connect compares the versions itself rather than relying on the warning.", + "min_cli_version": "4.1.4", + "manifest_source": "https://github.com/fluree/db/releases/latest/download/fluree-cli-manifest.json", + "commands": [ + { "path": ["init"] }, + { "path": ["list"], "flags": ["remote"] }, + { "path": ["create"], "flags": ["from"] }, + { "path": ["publish"] }, + { "path": ["remote", "add"], "flags": ["token"] }, + { "path": ["auth", "login"], "flags": ["remote", "token"] }, + { "path": ["auth", "status"], "flags": ["remote"] }, + { "path": ["auth", "token"], "flags": ["remote"] }, + { "path": ["iceberg", "map"] }, + { "path": ["iceberg", "list"] }, + { "path": ["iceberg", "info"] }, + { "path": ["model", "entity", "show"] }, + { "path": ["model", "access", "show"] } + ] +} diff --git a/plugins/fluree-companion/contract/stack-facts.json b/plugins/fluree-companion/contract/stack-facts.json new file mode 100644 index 0000000..2e82b99 --- /dev/null +++ b/plugins/fluree-companion/contract/stack-facts.json @@ -0,0 +1,95 @@ +{ + "contract_version": 1, + "comment": "Every Fluree AI (Solo) stack path, doc slug, and tool name this plugin's text hard-codes. CI validates it with scripts/check-stack-facts.mjs. Keep this SMALL: the stack-first doctrine exists so the knowledge lives on the stack, not here — this file exists for the handful of addresses the choreography cannot avoid naming.", + "manifest_source": "https://raw.githubusercontent.com/fluree/solo/main/src/contracts/generated/router/endpoint-manifest.json", + "docs_index_path": "/api/docs", + "router_endpoints": [ + { + "method": "GET", + "path": "/.well-known/fluree.json", + "public": true, + "why": "the pre-auth discovery probe the whole connect choreography opens with" + }, + { + "method": "GET", + "path": "/.well-known/oauth-protected-resource", + "public": true, + "why": "RFC 9728 — how an MCP client finds the authorization server for /v1/mcp" + }, + { + "method": "GET", + "path": "/.well-known/oauth-authorization-server", + "public": true, + "why": "RFC 8414 — the metadata `claude mcp add` reads to run the OAuth flow unattended" + }, + { + "method": "*", + "path": "/v1/mcp", + "public": false, + "why": "the Space MCP endpoint the plugin registers; auth is the reason the OAuth discovery above matters" + }, + { + "method": "GET", + "path": "/v1/fluree/system/version", + "public": false, + "why": "cited as the authenticated place a stack's exact version lives (well-known deliberately omits it)" + } + ], + "api_prefixes": [ + { + "prefix": "/v1/fluree", + "why": "the default api_base_url; `fluree remote add` takes the stack origin with this suffix" + } + ], + "ui_paths": [ + { + "method": "GET", + "path": "/api/docs", + "why": "llms.txt-style public docs index — the HTTP fallback when stack docs MCP tools are absent" + }, + { + "method": "GET", + "path": "/api/docs/{category}/{slug}", + "why": "raw markdown for one public page" + }, + { + "method": "GET", + "path": "/activate", + "why": "where the human approves the CLI's device code" + }, + { + "method": "GET", + "path": "/docs/{category}/{slug}", + "why": "the signed-in rendering of a page, including pages the public index withholds" + } + ], + "doc_slugs": [ + { "slug": "reference/cli", "served": "public" }, + { "slug": "reference/api", "served": "public" }, + { "slug": "reference/building-apps", "served": "public" }, + { "slug": "how-to/upload-and-import-data", "served": "public" }, + { "slug": "how-to/map-files-into-a-knowledge-graph", "served": "public" }, + { "slug": "concepts/how-data-gets-in", "served": "public" }, + { "slug": "concepts/spaces-tasks-and-agents", "served": "public" }, + { "slug": "how-to/connect-an-iceberg-catalog", "served": "public" }, + { "slug": "how-to/create-a-dataset-from-iceberg", "served": "public" }, + { "slug": "concepts/iceberg-r2rml-mapping", "served": "public" }, + { "slug": "how-to/publish-a-ledger-with-the-cli", "served": "public" }, + { "slug": "how-to/connect-a-remote-fluree", "served": "public" }, + { "slug": "how-to/access-policies", "served": "public" }, + { "slug": "concepts/policies-and-data-governance", "served": "public" }, + { + "slug": "how-to/connect-external-mcp-client", + "served": "in-app", + "why": "outside the public allowlist as of solo main — the plugin sends users to the signed-in /docs URL for it. Flip to public (and relink) if solo#1040 B4 widens the allowlist." + } + ], + "mcp_tools": [ + { "name": "docs_search", "surface": "stack", "probe_only": true }, + { "name": "docs_get", "surface": "stack", "probe_only": true }, + { "name": "docs_tree", "surface": "stack", "probe_only": true }, + { "name": "docs_examples", "surface": "stack", "probe_only": true }, + { "name": "sparql_query", "surface": "space", "probe_only": true }, + { "name": "get_data_model", "surface": "space", "probe_only": true } + ] +} diff --git a/plugins/fluree-companion/skills/fluree-companion/SKILL.md b/plugins/fluree-companion/skills/fluree-companion/SKILL.md new file mode 100644 index 0000000..5fd1d97 --- /dev/null +++ b/plugins/fluree-companion/skills/fluree-companion/SKILL.md @@ -0,0 +1,58 @@ +--- +name: fluree-companion +description: "Work against a deployed Fluree AI (Fluree Solo) stack — connect Claude Code to it, then let the stack itself teach you how to use it. Use whenever someone has a Fluree AI / Solo stack URL and wants to connect, register a Space's MCP server, or asks how to do something on their stack (upload and extract documents, expose a Space over MCP, connect an Iceberg catalog, govern a graph, build an app against it). Also trigger when a stack's /.well-known/fluree.json, /api/docs, an /activate device-code page, or a https:///v1/mcp?space=… URL turns up in the conversation." +--- + +# Working against a Fluree AI stack + +A Fluree AI (Fluree Solo) stack is a **deployment** — one customer's AWS stack, running one release, with its own datasets, Spaces, policies, and capabilities. This skill is the choreography for connecting to one and then working *through* it. It deliberately carries almost no platform reference, because the stack serves its own. + +The companion to this one is the **`fluree-cli`** skill, which covers driving the `fluree` binary itself. Reach for that when the work is CLI mechanics (flags, local ledgers, imports, policy authoring); reach for this one when the work is *a stack* (connecting, what this stack can do, where its knowledge lives). + +## Rule 1 — the stack is the source of truth; probe, don't recall + +Never answer a question about the platform from trained knowledge. Two stacks on different releases genuinely differ, and the stack will tell you which one you're on: + +- **Capabilities and compatibility:** `GET /.well-known/fluree.json` — unauthenticated, so it works before anything is configured. It carries `api_base_url`, an `auth` block (OIDC device flow or token), `capabilities`, `import` modes, `cli.min_version`, and a `companion` pointer. +- **How-to knowledge:** if the stack's docs MCP tools (`docs_search`, `docs_get`, `docs_tree`, `docs_examples`) are connected in this session, use them — they are pinned to the release the stack is running. **Probe for them; do not assume them.** They are new, and a stack older than that release won't have them. When they're absent, fall back to fetching `GET /api/docs` (a public llms.txt-style index) and then the `/api/docs/{category}/{slug}` page it points at. Mechanics: `references/stack-knowledge.md`. +- **The exact version** is deliberately *not* in the public discovery document; it lives behind the authenticated `GET /v1/fluree/system/version`. Don't fingerprint a stack you aren't signed in to. + +If the docs and your memory disagree, the docs are right. If the docs don't cover it, say so rather than filling the gap from training data. + +## Rule 2 — connection choreography, and the human approves + +The order matters, and one step is not yours to take: + +1. **Discover** — fetch `/.well-known/fluree.json`. Report what the stack advertises, and check `cli.min_version` against the local `fluree --version`. +2. **`fluree remote add https:///v1/fluree`** — remotes are named aliases; every later command takes `--remote `, never a URL. +3. **`fluree auth login --remote `** — this prints a device code and blocks. **The human approves it in a browser at the stack's `/activate` page.** Never skip it, never try to automate it, never go hunting for a token to shortcut it. Run the command, tell the user to approve, then verify with `fluree auth status --remote `. +4. **Optionally register the Space MCP server** — `claude mcp add --transport http fluree-space "https:///v1/mcp?space="`. Claude Code discovers the OAuth configuration from the stack (RFC 9728 + RFC 8414) and prompts for sign-in; the Space owner must have enabled MCP access first. + +Full walkthrough including the failure modes and what each one actually means: `references/connect.md`. The guided version is `/fluree-companion:connect`. + +## Rule 3 — division of labor: CLI, Space MCP, UI + +Pick the surface by what the user is trying to do, and say which one you're using: + +| Surface | Use it for | Don't use it for | +| --- | --- | --- | +| **`fluree` CLI** (`--remote`) | Data and admin work against ledgers: create, insert/update, query, export, branches, imports, Iceberg mapping, governance via `fluree model` | Anything a Space governs — the CLI rides the *user's* credential and sees more than a Space deliberately exposes | +| **Space MCP** (`/v1/mcp?space=…`) | Querying and analysing a Space's governed data with the Space's own tools and agents | Administering the stack; the exposed tool set is the owner's selection, intentionally narrower | +| **The UI** | What only it does: enabling MCP access on a Space, granting dataset access, reviewing identity matches, the visual review gates in extraction workflows | Anything scriptable — if the CLI or API can do it, prefer that so the user gets a repeatable artifact | + +When something the user wants is missing from the CLI (a dataset that doesn't appear in `fluree list --remote`, a tool that isn't exposed over MCP), that is governance working. Ask the user to grant it in the UI; never work around it. + +## Rule 4 — safety rails + +1. **`.fluree/` holds live tokens** after `fluree auth login` — access *and* refresh. Confirm it is gitignored before running `fluree init` inside a repo. To script a token, `fluree auth token --remote ` prints exactly the access token and nothing else. +2. **A `cli.min_version` warning is a stop-and-upgrade signal, not a nag.** If the stack asks for a newer CLI than the one installed, stop and resolve it — the commands the stack's own docs teach may not exist in the older binary, and the failure shows up later as a confusing flag error. `/fluree-cli:setup` handles the upgrade (including the common case where a new binary is shadowed on PATH by an old one). +3. **Destructive operations against a stack get explicit confirmation with the target named** — dropping a dataset, dropping a branch, or overwriting an import destination affects shared, multi-user data, not a local scratch ledger. There is no undo. +4. **Don't paste stack-identifying secrets into the session** — bearer tokens, `config list` output, or `.env` files. Take the stack *origin* from the user; take the credentials from the device flow. + +## Rule 5 — knowledge is per-stack, and doesn't travel + +The stack's docs *are* its version. Whatever you learn from one stack — a capability, an endpoint, a doc page, whether a tool exists — is a fact about **that** stack at **that** release. Don't carry it to another stack, don't cache it across a version bump, and when the user has more than one stack configured, name which one you probed. Re-probe rather than remember. + +## Playbooks + +Short skeletons for the common stories — upload and extract, expose a Space over MCP, connect an Iceberg catalog, build an app — each pointing at the stack doc that actually carries the procedure: `references/stories.md`. They are pointers by design; the stack's copy is the one that matches the stack. diff --git a/plugins/fluree-companion/skills/fluree-companion/references/connect.md b/plugins/fluree-companion/skills/fluree-companion/references/connect.md new file mode 100644 index 0000000..7275966 --- /dev/null +++ b/plugins/fluree-companion/skills/fluree-companion/references/connect.md @@ -0,0 +1,76 @@ +# Connecting to a stack, end to end + +Four steps, in order. Each one is verifiable before you move on — don't chain them blind. + +## 1. Discover — `GET /.well-known/fluree.json` + +Public and unauthenticated, so it is the right first move on a bare URL: + +```bash +curl -fsS https:///.well-known/fluree.json +``` + +What's in it, and what each field is *for*: + +| Field | Why you care | +| --- | --- | +| `api_base_url` | Where the data API lives — `/v1/fluree` by default, absolute on some deployments | +| `auth` | `type: "oidc_device"` (issuer, client_id, scopes → the device flow below) or `type: "token"` (no OIDC; the user supplies a token) | +| `capabilities` | What this stack has switched on | +| `import` | Which upload handshake the CLI should negotiate, plus size ceilings | +| `cli.min_version` | The oldest CLI this stack expects to behave correctly | +| `companion` | Which plugin marketplace and plugins the stack points its users at | + +Report this back to the user in plain terms — it is the first thing that tells them their stack is reachable and what shape it's in. Compare `cli.min_version` against `fluree --version` now, not later. + +**If it fails:** a non-JSON body usually means you hit a CDN error page — check the origin, and give it no path (discovery ignores paths). A 404 means the stack predates the discovery endpoint; you can still connect, you just have no capability advertisement, so don't infer one. + +## 2. Add the remote + +```bash +fluree init # once per project directory +fluree remote add mystack https:///v1/fluree +``` + +- The `/v1/fluree` suffix is the robust form: discovery ignores the input path, and if discovery is unreachable the suffixed URL is stored as-is and still works. +- Remotes are **named aliases**. Every later command takes `--remote mystack` (or the compound positional `mystack/ledger`) — never a URL. +- `remote add` may warn that the stack expects a newer CLI. Treat that as a stop: resolve it before continuing (`/fluree-cli:setup`), because the commands the stack's docs teach may not exist in the older binary. + +## 3. Log in — the human approves + +```bash +fluree auth login --remote mystack +``` + +This prints a short device code and opens the stack's `/activate` page. **The user clicks Approve in their browser** (one click if they're already signed in) while the CLI polls. Your job is to run the command, immediately tell the user to go approve the code, and wait. + +Do not skip this, do not try to drive the browser, and do not go looking for a token that would let you bypass it. Verify before continuing: + +```bash +fluree auth status --remote mystack +fluree list --remote mystack +``` + +**If it fails:** an expired code just needs a re-run. `auth.type: "token"` in step 1 means the stack has no OIDC configured — use the manual path (`fluree remote add … --token @token.txt`, or `fluree auth login --remote mystack --token @-`) with a token the user supplies. An empty `fluree list` with a healthy `auth status` is not a failure: the stack may genuinely have no datasets yet, or this user's access may not include them. + +## 4. Optional — register the Space MCP server + +A **Space** can expose its governed data and agent tools as an MCP server. For Claude Code: + +```bash +claude mcp add --transport http fluree-space "https:///v1/mcp?space=" +``` + +Claude Code discovers the OAuth configuration from the stack itself (`/.well-known/oauth-protected-resource`, RFC 9728, pointing at `/.well-known/oauth-authorization-server`, RFC 8414) and prompts for sign-in. Each user connects as themselves and sees only what they already have access to. + +Two prerequisites live in the UI and are owner-only: **MCP access must be enabled on the Space**, and the owner chooses which tools are exposed. The Space's **MCP** tab is where the server URL, the `spaceId`, and the OAuth discovery URL are displayed — that tab is also where the user goes if the connection 401s. + +The full page for this is `how-to/connect-external-mcp-client`. It sits outside the stack's *public* doc set today, so link the user to `https:///docs/how-to/connect-external-mcp-client` in the signed-in UI rather than trying to fetch it from `/api/docs`. + +## 5. Verify knowledge access + +Before declaring the session connected, confirm you can actually read this stack's knowledge — that is the whole point of connecting. Probe for the stack's docs tools; if they're absent, fetch `/api/docs`. Either way, name in your status line which of the two you got. Mechanics and the fallback chain: `stack-knowledge.md`. + +## Status line + +Finish with one line the user can act on: stack origin, CLI version vs `cli.min_version`, remote alias and auth state, Space MCP registered or not, and which knowledge path is live (stack docs tools, or `/api/docs` over HTTP). diff --git a/plugins/fluree-companion/skills/fluree-companion/references/stack-knowledge.md b/plugins/fluree-companion/skills/fluree-companion/references/stack-knowledge.md new file mode 100644 index 0000000..4f68ac0 --- /dev/null +++ b/plugins/fluree-companion/skills/fluree-companion/references/stack-knowledge.md @@ -0,0 +1,49 @@ +# Reading a stack's own knowledge + +## Three corpora, and they are not interchangeable + +| Source | Covers | Pinned to | +| --- | --- | --- | +| The `fluree` binary's embedded docs (`fluree docs search`, or the `fluree-docs` MCP server the `fluree-cli` plugin connects) | The **CLI's** command surface — flags, subcommands, local ledger mechanics | The installed binary's version | +| The **stack's** docs (`docs_search` etc. if present, else `GET /api/docs`) | The **platform** — Spaces, agents, datasets, imports, policies, Iceberg, apps, the UI, the HTTP API | The release that stack is running | +| This plugin | Choreography and doctrine only | Nothing — which is why it must not carry platform reference | + +A CLI flag question goes to the binary. "How do I do X in Fluree AI" goes to the stack. Answering either from training data is the failure mode this whole design exists to prevent. + +## Path A — the stack's docs MCP tools + +If `docs_search`, `docs_get`, `docs_tree`, or `docs_examples` are available in the session, use them. They are the good path: search is section-level, and results come back pinned to the stack's release. + +**Probe, don't assume.** These tools are new. A stack running an older release will not have them, and neither will a session that hasn't registered the stack's MCP server. Check what tools you actually have before planning around them; on absence, fall straight to Path B without commentary. + +## Path B — HTTP fallback via `/api/docs` + +Every stack serves an llms.txt-style index at `GET /api/docs`, unauthenticated, as markdown: + +```bash +curl -fsS https:///api/docs +``` + +It opens with a header, then one section per category (Tutorials, How-to Guides, Concepts, Reference), each a list of lines shaped like: + +``` +- [How to Upload and Import Data](/api/docs/how-to/upload-and-import-data) — +``` + +So the chain is: **fetch the index → pick slugs by title and description → fetch each page's raw markdown.** + +```bash +curl -fsS https:///api/docs/how-to/upload-and-import-data +``` + +There is no search endpoint in this path — the index *is* the search surface, and it is small enough to read in full. Pick from it rather than guessing a slug: fetching an unknown slug returns 404, and a slug that exists on one stack may not exist on another. + +## What the public index leaves out + +The `/api/docs` set is narrower than the docs a signed-in user sees in the UI. Pages outside it 404 on `/api/docs/...` while rendering fine at `https:///docs/{category}/{slug}` for a signed-in user. `how-to/connect-external-mcp-client` is one of them today. + +So: if a page you expect isn't in the index, that is not evidence the stack lacks the feature. Point the user at the signed-in `/docs/...` URL instead, and say why. + +## Citing what you read + +When you answer from a stack doc, say which page and which stack. "Per `how-to/upload-and-import-data` on your stack" is a checkable claim; "Fluree supports…" is not, and is exactly the kind of statement that ages badly across releases. diff --git a/plugins/fluree-companion/skills/fluree-companion/references/stories.md b/plugins/fluree-companion/skills/fluree-companion/references/stories.md new file mode 100644 index 0000000..f49acc3 --- /dev/null +++ b/plugins/fluree-companion/skills/fluree-companion/references/stories.md @@ -0,0 +1,43 @@ +# Playbook skeletons + +Each entry is a **route**, not a procedure: the goal, the stack doc that carries the actual steps, and the entry point to start from. Fetch the doc (see `stack-knowledge.md`) before doing any of it — the stack's copy matches the stack, and this file cannot. + +Slugs below are `category/slug` on the stack's own docs: `https:///api/docs/` for raw markdown, `https:///docs/` signed-in. + +--- + +## Get files into the knowledge graph + +**Goal:** documents or structured files land in a dataset as queryable graph data. +**Knowledge:** `how-to/upload-and-import-data`, then `how-to/map-files-into-a-knowledge-graph` for the extraction/mapping half. `concepts/how-data-gets-in` explains why the paths have different size ceilings. +**Entry points:** the UI's Upload tab for the interactive path; `fluree create --from` for a bulk local import (mind the memory budget on a shared machine — the `fluree-cli` skill's rail); the negotiated upload handshake for large archives, whose modes the stack advertises in `/.well-known/fluree.json` under `import`. + +## Publish a local ledger to the stack + +**Goal:** work built locally with the CLI becomes a dataset on the stack. +**Knowledge:** `how-to/publish-a-ledger-with-the-cli`. `how-to/connect-a-remote-fluree` covers the other direction — mounting someone else's Fluree as read-only datasets. +**Entry points:** `fluree publish`, against a remote already connected per `connect.md`. + +## Expose a Space over MCP + +**Goal:** a Space's governed data and tools become an MCP server that Claude Code (or Claude Desktop, or claude.ai) can use. +**Knowledge:** `how-to/connect-external-mcp-client` — **not in the public `/api/docs` set today**, so send the user to the signed-in `https:///docs/how-to/connect-external-mcp-client`. `concepts/spaces-tasks-and-agents` for what a Space is. +**Entry points:** the Space's **MCP** tab (owner-only: enable access, choose exposed tools, read off the server URL and `spaceId`), then `claude mcp add --transport http fluree-space "https:///v1/mcp?space="`. + +## Connect an Iceberg catalog + +**Goal:** query Iceberg tables as graph data without copying them. +**Knowledge:** `how-to/connect-an-iceberg-catalog`, then `how-to/create-a-dataset-from-iceberg`. `concepts/iceberg-r2rml-mapping` explains what the mapping actually does. +**Entry points:** the `fluree iceberg` command family (`map`, `list`, `info`) — check the installed binary's `--help` or its embedded docs for flags, per the `fluree-cli` skill. + +## Govern a graph + +**Goal:** policies and shapes that constrain what a dataset accepts and who can read it. +**Knowledge:** `how-to/access-policies` and `concepts/policies-and-data-governance`. +**Entry points:** the `fluree model` family (it compiles to ordinary SHACL/policy data in the ledger — read back what's enforced with `model entity show` / `model access show`). Verify policy behaviour on **local/direct** execution; the `fluree-cli` skill's policy reference explains why server-routed verification misleads. + +## Build an app against the stack + +**Goal:** application code talking to the stack's HTTP API. +**Knowledge:** `reference/building-apps` is the recipe and is deliberately inside the public doc set, so it is fetchable with no credentials at all; `reference/api` is the endpoint reference; `reference/cli` covers the CLI-side surface. +**Entry points:** the stack origin and a Space id are the only instance-specific inputs; everything else comes from those pages. diff --git a/scripts/check-stack-facts.mjs b/scripts/check-stack-facts.mjs new file mode 100644 index 0000000..dbd3b61 --- /dev/null +++ b/scripts/check-stack-facts.mjs @@ -0,0 +1,261 @@ +#!/usr/bin/env node +/** + * Validate a plugin's contract/stack-facts.json against the artifacts a + * Fluree AI (Solo) stack publishes about itself. + * + * Usage: + * node scripts/check-stack-facts.mjs [] + * + * The manifest argument defaults to the facts file's `manifest_source`. A + * value starting with http(s) is fetched; anything else is read from disk + * (handy for validating against a solo working tree before the generated + * artifact is published). + * + * Two halves, because a stack's surface has two producers: + * + * 1. ROUTER — `src/contracts/generated/router/endpoint-manifest.json`, + * generated from the Rust router's own route table. Every path the + * plugin's prose hard-codes must resolve against it, and every path the + * prose calls unauthenticated must actually carry `requireAuth: false` + * (the connect choreography's whole premise is a pre-auth probe). + * 2. UI LAMBDA — `/api/docs`, `/activate`, `/docs/...` are served by the + * Next.js UI, not the router, so the manifest cannot confirm them. What + * it CAN falsify is the ownership claim: if one of them appears in the + * router manifest, the split the prose describes has moved. Their doc + * slugs are checked against a live stack instead, when STACK_ORIGIN is + * set — otherwise that half is skipped with a note. + * + * Doc slugs carry a `served` field, and both values are assertions: + * `public` slugs MUST appear in the live `/api/docs` index (the plugin links + * them as fetchable), and `in-app` slugs must NOT (the plugin tells users to + * open them in the signed-in UI, and would be wrong to hand out a URL that + * 404s). The `in-app` direction can't distinguish "withheld from the public + * allowlist" from "deleted" — it catches the promotion, not the deletion. + * + * Exit codes: 0 ok, 1 contract violations, 2 usage/IO/malformed input. + */ + +import { readFileSync } from "node:fs"; + +const [factsPath, manifestArg] = process.argv.slice(2); +if (!factsPath) { + console.error( + "usage: check-stack-facts.mjs []", + ); + process.exit(2); +} + +function parseJson(raw, label, source) { + try { + return JSON.parse(raw); + } catch (e) { + // The realistic path here: a fetch "succeeded" on an HTML 404 page. That + // is an infrastructure problem, not a contract violation — exit 2 so a + // maintainer doesn't start editing stack-facts.json over it. + console.error(`${label} at ${source} is not valid JSON: ${e.message}`); + process.exit(2); + } +} + +function loadLocalJson(path, label) { + let raw; + try { + raw = readFileSync(path, "utf-8"); + } catch (e) { + console.error(`cannot read ${label} at ${path}: ${e.message}`); + process.exit(2); + } + return parseJson(raw, label, path); +} + +async function fetchText(url, label) { + let res; + try { + res = await fetch(url); + } catch (e) { + console.error(`cannot fetch ${label} from ${url}: ${e.message}`); + process.exit(2); + } + if (!res.ok) { + console.error(`cannot fetch ${label} from ${url}: HTTP ${res.status}`); + process.exit(2); + } + return res.text(); +} + +const facts = loadLocalJson(factsPath, "stack-facts"); +if (facts.contract_version !== 1) { + console.error( + `contract_version ${facts.contract_version} unsupported (checker knows 1); update this script.`, + ); + process.exit(2); +} + +const manifestSource = manifestArg ?? facts.manifest_source; +if (!manifestSource) { + console.error( + "no manifest source: pass one as argv[2] or set `manifest_source` in the facts file.", + ); + process.exit(2); +} + +const manifest = /^https?:\/\//.test(manifestSource) + ? parseJson( + await fetchText(manifestSource, "endpoint manifest"), + "endpoint manifest", + manifestSource, + ) + : loadLocalJson(manifestSource, "endpoint manifest"); + +if (manifest.version !== 1) { + console.error( + `endpoint manifest version ${manifest.version} unsupported (checker knows 1); update this script.`, + ); + process.exit(2); +} +if (!manifest.routes || typeof manifest.routes !== "object") { + console.error("malformed endpoint manifest: no `routes` object."); + process.exit(2); +} + +/** `{ledgerId}` and `{*rest}` are the same hole to a consumer of the manifest. */ +const normalizeTemplate = (p) => p.replace(/\{[^}]*\}/g, "{}"); +/** A concrete path to test against a regex row. */ +const concretize = (p) => p.replace(/\{[^}]*\}/g, "_"); + +const rows = Object.entries(manifest.routes).map(([key, value]) => { + const [method, ...rest] = key.split(" "); + const template = rest.join(" ").replace(/ \[regex\]$/, ""); + return { key, method, template, ...value }; +}); + +const methodMatches = (rowMethod, factMethod) => + rowMethod === "*" || factMethod === "*" || rowMethod === factMethod; + +function resolveRoute(method, path) { + const wanted = normalizeTemplate(path); + // matchit beats regex (the manifest's own precedence), so try it first. + const exact = rows.find( + (r) => + r.kind === "matchit" && + methodMatches(r.method, method) && + normalizeTemplate(r.template) === wanted, + ); + if (exact) return exact; + const concrete = concretize(path); + return rows.find( + (r) => + r.kind === "regex" && + methodMatches(r.method, method) && + r.regex && + new RegExp(r.regex).test(concrete), + ); +} + +const errors = []; +const notes = []; +let checked = 0; + +for (const entry of facts.router_endpoints ?? []) { + checked += 1; + const { method = "*", path, public: isPublic } = entry; + const row = resolveRoute(method, path); + if (!row) { + errors.push(`endpoint not in router manifest: ${method} ${path}`); + continue; + } + if (typeof isPublic === "boolean") { + const requiresAuth = row.authFlags?.requireAuth === true; + if (isPublic && requiresAuth) { + errors.push( + `${method} ${path} is documented as unauthenticated but the manifest has requireAuth: true (matched ${row.key})`, + ); + } else if (!isPublic && !requiresAuth) { + errors.push( + `${method} ${path} is documented as authenticated but the manifest has requireAuth: false (matched ${row.key})`, + ); + } + } +} + +for (const entry of facts.api_prefixes ?? []) { + checked += 1; + const prefix = entry.prefix; + if (!rows.some((r) => r.template.startsWith(prefix))) { + errors.push( + `api prefix matches no router route: ${prefix} (the prose builds URLs on it)`, + ); + } +} + +// UI-Lambda paths: the manifest is the falsifier for the OWNERSHIP claim, +// not for existence. A hit here means a path the plugin describes as +// UI-served is now routed by the router — the split the prose teaches moved. +for (const entry of facts.ui_paths ?? []) { + checked += 1; + const { method = "GET", path } = entry; + const row = resolveRoute(method, path); + if (row) { + errors.push( + `${method} ${path} is documented as UI-Lambda-served but now resolves in the router manifest (${row.key}) — re-check the prose's CLI/UI/API split`, + ); + } +} + +const slugs = facts.doc_slugs ?? []; +const stackOrigin = process.env.STACK_ORIGIN?.replace(/\/+$/, ""); +if (slugs.length > 0 && stackOrigin) { + const indexUrl = `${stackOrigin}/api/docs`; + const index = await fetchText(indexUrl, "docs index"); + const served = new Set( + [...index.matchAll(/\/api\/docs\/([a-z0-9-]+)\/([a-zA-Z0-9._-]+)/g)].map( + (m) => `${m[1]}/${m[2]}`, + ), + ); + if (served.size === 0) { + console.error( + `docs index at ${indexUrl} listed no /api/docs/{category}/{slug} links — wrong origin, or the index format changed.`, + ); + process.exit(2); + } + for (const entry of slugs) { + checked += 1; + const isServed = served.has(entry.slug); + if (entry.served === "public" && !isServed) { + errors.push( + `doc slug missing from ${indexUrl}: ${entry.slug} (the plugin links it as publicly fetchable)`, + ); + } else if (entry.served === "in-app" && isServed) { + errors.push( + `doc slug is now public at ${indexUrl}: ${entry.slug} — the plugin should link the /api/docs URL instead of sending users to the signed-in UI`, + ); + } + } + notes.push(`docs index checked against ${indexUrl} (${served.size} served).`); +} else if (slugs.length > 0) { + notes.push( + `docs slugs NOT checked (${slugs.length} skipped): set STACK_ORIGIN=https:// to validate them against a live /api/docs index.`, + ); +} + +// Stack MCP tool names have no published producer artifact yet (solo#1040 +// B2). The plugin's prose probes for them rather than asserting them, so +// they are recorded here for review, not validated. +const probeTools = (facts.mcp_tools ?? []).filter((t) => t.probe_only); +if (probeTools.length > 0) { + notes.push( + `mcp tools unvalidated (probe-only, no producer artifact yet): ${probeTools.map((t) => t.name).join(", ")}.`, + ); +} + +for (const note of notes) console.log(`note: ${note}`); + +if (errors.length > 0) { + console.error(`stack-facts contract violated (${errors.length}):`); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); +} + +console.log( + `stack-facts contract OK against endpoint manifest v${manifest.version} (${checked} facts checked).`, +); From fba0ecdc4c83e42db2e0ed436a0f1e7586e0887d Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Sat, 15 Aug 2026 17:47:03 -0400 Subject: [PATCH 3/5] docs(fluree-cli): point at fluree-companion for stack-first work (1.0.2) Whichever plugin a user finds first should point at the rest. The remote rule already owned the connection choreography, so it's the natural place to hand off: stack-shaped work (what can this deployment do, where does its knowledge live) goes to the companion, and the companion routes data and admin operations back here. --- .claude-plugin/marketplace.json | 2 +- plugins/fluree-cli/.claude-plugin/plugin.json | 2 +- plugins/fluree-cli/README.md | 4 ++++ plugins/fluree-cli/skills/fluree-cli/SKILL.md | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0e3cd19..3d80e84 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -24,7 +24,7 @@ "name": "fluree-cli", "source": "./plugins/fluree-cli", "description": "Companion for driving the fluree CLI well — probe-first doctrine, safety rails, and Fluree AI (Solo) remote workflows. Defers all command reference to the binary's own embedded docs.", - "version": "1.0.1" + "version": "1.0.2" } ] } diff --git a/plugins/fluree-cli/.claude-plugin/plugin.json b/plugins/fluree-cli/.claude-plugin/plugin.json index 1a850bb..4010b67 100644 --- a/plugins/fluree-cli/.claude-plugin/plugin.json +++ b/plugins/fluree-cli/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "fluree-cli", "description": "Companion for driving the fluree CLI well — probe-first doctrine, safety rails, and Fluree AI (Solo) remote workflows. Defers all command reference to the binary's own embedded docs.", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Fluree Dev Rel" }, diff --git a/plugins/fluree-cli/README.md b/plugins/fluree-cli/README.md index 03f113f..16c7479 100644 --- a/plugins/fluree-cli/README.md +++ b/plugins/fluree-cli/README.md @@ -13,6 +13,10 @@ Teaches Claude Code to drive the [`fluree`](https://github.com/fluree/db) CLI sa Requires **fluree ≥ 4.1.4** (the floor set by `fluree auth token`; the `fluree model` governance family needs 4.1.3+). +## Working against a deployed stack? + +Install **[fluree-companion](../fluree-companion/README.md)** alongside this one. It owns the stack-first half: discovery via `/.well-known/fluree.json`, the guided `/fluree-companion:connect`, registering a Space's MCP server, and reading a stack's own version-pinned docs rather than recalling platform knowledge. This plugin stays the authority on the binary; that one routes the data and admin work back here. + ## Install ```text diff --git a/plugins/fluree-cli/skills/fluree-cli/SKILL.md b/plugins/fluree-cli/skills/fluree-cli/SKILL.md index 203b9ff..2c1bf49 100644 --- a/plugins/fluree-cli/skills/fluree-cli/SKILL.md +++ b/plugins/fluree-cli/skills/fluree-cli/SKILL.md @@ -37,6 +37,8 @@ Prefer structured output when parsing: `query` and `multi-query` take `--format `--remote ` takes a configured **alias**, never a URL. The full connection choreography — `remote add`, the device-code login where the *human* approves in a browser, token scripting, and what differs between local and server-routed execution — is in `references/remote-fluree-ai.md`. The one thing to never forget: **`fluree auth login` blocks on the user approving a device code at the stack's `/activate` page** — run it, tell the user to approve, and verify with `fluree auth status` before continuing. +When the work is really *about the stack* rather than about the CLI — connecting a fresh machine to one, asking what a particular stack can do, or answering a how-do-I question whose answer lives in the stack's own docs — that is the **`fluree-companion`** plugin's job (same marketplace; `/fluree-companion:connect` is its guided setup). It probes `/.well-known/fluree.json` for capabilities and the minimum CLI version, reads the stack's version-pinned docs instead of recalling platform knowledge, and hands data and admin operations back to this skill. + ## Common flows Step-by-step recipes (create→insert→query, bulk import, branch/merge, Iceberg mapping, app-building against a Fluree AI stack): `references/workflows.md`. Debugging divergences (server-routed vs `--direct`, time-travel formatting rules, auto-routing): `references/troubleshooting.md`. From 92c7c122e4298689f0cbb04ea2afc34edcbfd220 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Sat, 15 Aug 2026 17:47:17 -0400 Subject: [PATCH 4/5] docs: reposition the marketplace for customers, not employees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo self-described as "Internal Claude Code plugins for Fluree demos and tooling" and the README assumed a reader who already worked here. It now leads with the three-plugin mental model — your stack, the CLI that drives it, data to put in it — install paths for all three, and the reason the knowledge lives in the stack and the binary rather than in a plugin. Brand review is separate; this is the factual pass. --- .claude-plugin/marketplace.json | 2 +- README.md | 115 +++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3d80e84..fc07466 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,7 +5,7 @@ "email": "ajohnson@flur.ee" }, "metadata": { - "description": "Internal Claude Code plugins for Fluree demos and tooling" + "description": "Claude Code plugins for working with Fluree AI — connecting to your stack, driving the fluree CLI, and generating demo datasets" }, "plugins": [ { diff --git a/README.md b/README.md index 7f140f7..82674af 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,34 @@ # Fluree Claude Plugins -Internal [Claude Code](https://code.claude.com/docs) plugins for Fluree demos and tooling. +[Claude Code](https://code.claude.com/docs) plugins for working with **Fluree AI** — connecting +to your stack, driving the `fluree` CLI, and generating datasets to load into it. This repository is a **Claude Code plugin marketplace**. Add it once, then install any plugin inside it from Claude Code — either the **Claude Desktop** app (Code mode) or the **Claude Code CLI** in a terminal. -> New here and just want to make demo data for Fluree? Jump to -> [Installation](#installation), install **fluree-dataset-generator**, then follow its -> [usage guide](plugins/fluree-dataset-generator/README.md). +> Have a Fluree AI stack and want Claude Code working against it? Jump to +> [Installation](#installation), install **fluree-companion**, then run +> `/fluree-companion:connect`. --- ## What's inside +Three plugins, and the mental model behind them: **your stack**, **the CLI that drives it**, +and **data to put in it**. Install the ones that match what you're doing — they're independent, +and `fluree-companion` + `fluree-cli` are designed to be used together. + | Plugin | What it does | Docs | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| **fluree-dataset-generator** | Interviews you about a domain and generates a complete, **validated** JSON-LD dataset (RDF model + realistic instance data) ready to upload into a Fluree knowledge graph such as Fluree Solo. | [README](plugins/fluree-dataset-generator/README.md) | +| **fluree-companion** | Connects Claude Code to **your Fluree AI (Solo) stack** and works against it — capability discovery, the human-approved device login, Space MCP registration, and playbooks that read your stack's own version-pinned docs instead of guessing. Includes `/fluree-companion:connect`. | [README](plugins/fluree-companion/README.md) | | **fluree-cli** | Teaches Claude Code to drive the **`fluree` CLI** safely and well — probe-first use of the binary's embedded docs, destructive-op and credential safety rails, and Fluree AI (Solo) remote workflows. Includes `/fluree-cli:setup`. | [README](plugins/fluree-cli/README.md) | +| **fluree-dataset-generator** | Interviews you about a domain and generates a complete, **validated** JSON-LD dataset (RDF model + realistic instance data) ready to load into a Fluree knowledge graph. | [README](plugins/fluree-dataset-generator/README.md) | + +Why a plugin rather than a hosted service: every Fluree AI stack is its own deployment running +its own release. So the plugins carry *choreography* — how to connect, what order to do things +in, what not to do — while the knowledge itself stays in your stack and in your `fluree` binary, +where it always matches the version you're actually running. _Marketplace name: `fluree-plugins` (this is what you reference when installing — see below)._ @@ -59,8 +70,9 @@ You only add the marketplace once; afterward you can install/update plugins free ```text /plugin marketplace add fluree/claude-plugins -/plugin install fluree-dataset-generator@fluree-plugins +/plugin install fluree-companion@fluree-plugins /plugin install fluree-cli@fluree-plugins +/plugin install fluree-dataset-generator@fluree-plugins ``` Notes: @@ -98,8 +110,8 @@ always the same: **`Personal`**. 8. Click the **`+`** icon to add a plugin repository → **`Add marketplace from GitHub`**. 9. Type **`fluree/claude-plugins`** and click **`Sync`**. -10. A **`Fluree dataset generator`** card appears. Hover it and click **`Install`**. Once - installed, the button changes to **`Manage`**. +10. A card appears for each plugin in this marketplace. Hover the one you want and click + **`Install`**. Once installed, the button changes to **`Manage`**. 11. Back out to the main session with **`Code`** selected at the top — you're ready to use it. ### Option C — Team / non-interactive (settings.json) @@ -115,7 +127,8 @@ To pre-register the marketplace and auto-enable plugins for a project or team, a } }, "enabledPlugins": { - "fluree-dataset-generator@fluree-plugins": true + "fluree-companion@fluree-plugins": true, + "fluree-cli@fluree-plugins": true } } ``` @@ -129,20 +142,23 @@ commands. Project-scoped marketplaces prompt users to trust them before loading. Once installed, each plugin contributes a **Skill** that you can invoke three ways: -- **Ask in plain English** — skills auto-trigger on intent, e.g. _"generate a demo insurance - dataset for Fluree"_ or _"connect the fluree CLI to my stack."_ You don't have to name the - plugin. +- **Ask in plain English** — skills auto-trigger on intent, e.g. _"connect Claude Code to my + Fluree stack"_, _"generate a demo insurance dataset for Fluree"_, or _"query this ledger from + the CLI."_ You don't have to name the plugin. - **Desktop menu** — `+` → **Plugins → \ → Skills** (inserts the skill's slash command, then press **Enter**). -- **CLI slash command** — `/fluree-dataset-generator:fluree-dataset-generator`, or for the - CLI companion `/fluree-cli:setup` (its skill auto-triggers; `setup` is the explicit entry - point). - -Each plugin's own README has the full usage guide. For the dataset generator — including the -interview questions, scale guidance, and how to **upload the output into Fluree Solo** — see +- **CLI slash command** — `/fluree-companion:connect` to set up a stack connection, + `/fluree-cli:setup` to set up the binary, or + `/fluree-dataset-generator:fluree-dataset-generator` to generate data. (The skills + auto-trigger; these are the explicit entry points.) + +Each plugin's own README has the full usage guide — the stack companion's connection +choreography and contract checks in +**[plugins/fluree-companion/README.md](plugins/fluree-companion/README.md)**, what the CLI skill +enforces and how it stays in sync with the binary in +**[plugins/fluree-cli/README.md](plugins/fluree-cli/README.md)**, and the dataset generator's +interview questions, scale guidance, and load instructions in **[plugins/fluree-dataset-generator/README.md](plugins/fluree-dataset-generator/README.md)**. -For the CLI companion — what the skill enforces, the MCP docs server, and how it stays in -sync with the CLI — see **[plugins/fluree-cli/README.md](plugins/fluree-cli/README.md)**. --- @@ -152,7 +168,7 @@ sync with the CLI — see **[plugins/fluree-cli/README.md](plugins/fluree-cli/RE | ---------------------------- | ---------------------------------------------------------------- | | Open the plugin UI | `/plugin` | | Update this marketplace | `/plugin marketplace update fluree-plugins` | -| Update an installed plugin | re-run `/plugin install fluree-dataset-generator@fluree-plugins` | +| Update an installed plugin | re-run `/plugin install fluree-companion@fluree-plugins` | | Enable / disable / uninstall | `/plugin` → **Installed** tab | In Claude Desktop, the **`Manage`** button on a plugin card (or **Plugins → Manage Plugins**) @@ -166,32 +182,55 @@ covers update/disable/uninstall. claude-plugins/ ├── .claude-plugin/ │ └── marketplace.json # marketplace manifest (name: "fluree-plugins") -├── .github/workflows/ci.yml # validate --strict, version sync, CLI contract check +├── .github/workflows/ci.yml # validate --strict, version sync, contract checks ├── scripts/ │ ├── check-version-sync.mjs # marketplace ↔ plugin.json version invariant -│ └── check-cli-contract.mjs # cli-facts vs the fluree CLI's release manifest +│ ├── check-cli-contract.mjs # cli-facts vs the fluree CLI's release manifest +│ └── check-stack-facts.mjs # stack-facts vs solo's router endpoint manifest └── plugins/ - ├── fluree-dataset-generator/ + ├── fluree-companion/ │ ├── .claude-plugin/ - │ │ └── plugin.json # plugin manifest (name, version, author) - │ ├── README.md # plugin usage guide + │ │ └── plugin.json + │ ├── README.md + │ ├── commands/connect.md # /fluree-companion:connect + │ ├── contract/ + │ │ ├── stack-facts.json # every stack path/slug/tool the plugin names + │ │ └── cli-facts.json # the fluree commands its choreography names │ └── skills/ - │ └── fluree-dataset-generator/ - │ ├── SKILL.md # the skill's instructions to Claude - │ └── scripts/ # Python validators run during generation - └── fluree-cli/ + │ └── fluree-companion/ + │ ├── SKILL.md # stack-first operating doctrine + │ └── references/ # connect choreography, stack knowledge, playbooks + ├── fluree-cli/ + │ ├── .claude-plugin/ + │ │ └── plugin.json + │ ├── README.md + │ ├── .mcp.json # auto-connects `fluree mcp serve --toolsets docs` + │ ├── commands/setup.md # /fluree-cli:setup + │ ├── contract/cli-facts.json # every CLI fact the plugin hard-codes (CI-checked) + │ └── skills/ + │ └── fluree-cli/ + │ ├── SKILL.md # probe-first operating doctrine + │ └── references/ # workflows, remote/Fluree AI, policy, troubleshooting + └── fluree-dataset-generator/ ├── .claude-plugin/ - │ └── plugin.json - ├── README.md - ├── .mcp.json # auto-connects `fluree mcp serve --toolsets docs` - ├── commands/setup.md # /fluree-cli:setup - ├── contract/cli-facts.json # every CLI fact the plugin hard-codes (CI-checked) + │ └── plugin.json # plugin manifest (name, version, author) + ├── README.md # plugin usage guide └── skills/ - └── fluree-cli/ - ├── SKILL.md # probe-first operating doctrine - └── references/ # workflows, remote/Fluree AI, policy, troubleshooting + └── fluree-dataset-generator/ + ├── SKILL.md # the skill's instructions to Claude + └── scripts/ # Python validators run during generation ``` +### How the plugins stay honest + +Both companions defer their reference material to a producer — the `fluree` binary's embedded +docs, and your stack's own served docs — so they can't go stale when either one ships a new +release. The few facts their prose *does* hard-code live in a `contract/` file per plugin and +are validated in CI against an artifact the producer publishes: `cli-facts.json` against the +`fluree-cli-manifest.json` release asset from fluree/db, and `stack-facts.json` against the +generated router endpoint manifest from fluree/solo. If a release changes something a plugin +says, CI fails here before a user hits it. + --- ## For maintainers — adding a plugin From 98994ef1af49b7797176162efe94740b14a56e94 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 17 Aug 2026 12:58:55 -0400 Subject: [PATCH 5/5] fix(fluree-companion): probe-shaped prose where the plugin asserted today's stack state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review-pass remediation. Two references made flat claims that go stale the moment solo#1041 merges — 'there is no search endpoint in this path' (that branch ships GET /api/docs/search, and the stack's own index header advertises it) and 'connect-external-mcp-client is not in the public set today' (the allowlist widens to the full corpus). Both were the plugin shipping its own Rule-1 counterexample: a today-shaped assertion about all stacks. Both are now probes — try the search endpoint, fall back to the index on 404; if the public page fetch 404s, route to the signed-in URL — which is correct for every stack version simultaneously and lets the two PRs merge in either order. stack-facts gains /api/docs/search under the inverted UI-path check, and the slug flips to public with a version note, removing the silent hand-flip obligation. --- .../contract/stack-facts.json | 124 ++++++++++++++---- .../references/stack-knowledge.md | 4 +- .../fluree-companion/references/stories.md | 2 +- 3 files changed, 101 insertions(+), 29 deletions(-) diff --git a/plugins/fluree-companion/contract/stack-facts.json b/plugins/fluree-companion/contract/stack-facts.json index 2e82b99..875fce0 100644 --- a/plugins/fluree-companion/contract/stack-facts.json +++ b/plugins/fluree-companion/contract/stack-facts.json @@ -1,6 +1,6 @@ { "contract_version": 1, - "comment": "Every Fluree AI (Solo) stack path, doc slug, and tool name this plugin's text hard-codes. CI validates it with scripts/check-stack-facts.mjs. Keep this SMALL: the stack-first doctrine exists so the knowledge lives on the stack, not here — this file exists for the handful of addresses the choreography cannot avoid naming.", + "comment": "Every Fluree AI (Solo) stack path, doc slug, and tool name this plugin's text hard-codes. CI validates it with scripts/check-stack-facts.mjs. Keep this SMALL: the stack-first doctrine exists so the knowledge lives on the stack, not here \u2014 this file exists for the handful of addresses the choreography cannot avoid naming.", "manifest_source": "https://raw.githubusercontent.com/fluree/solo/main/src/contracts/generated/router/endpoint-manifest.json", "docs_index_path": "/api/docs", "router_endpoints": [ @@ -14,13 +14,13 @@ "method": "GET", "path": "/.well-known/oauth-protected-resource", "public": true, - "why": "RFC 9728 — how an MCP client finds the authorization server for /v1/mcp" + "why": "RFC 9728 \u2014 how an MCP client finds the authorization server for /v1/mcp" }, { "method": "GET", "path": "/.well-known/oauth-authorization-server", "public": true, - "why": "RFC 8414 — the metadata `claude mcp add` reads to run the OAuth flow unattended" + "why": "RFC 8414 \u2014 the metadata `claude mcp add` reads to run the OAuth flow unattended" }, { "method": "*", @@ -45,13 +45,18 @@ { "method": "GET", "path": "/api/docs", - "why": "llms.txt-style public docs index — the HTTP fallback when stack docs MCP tools are absent" + "why": "llms.txt-style public docs index \u2014 the HTTP fallback when stack docs MCP tools are absent" }, { "method": "GET", "path": "/api/docs/{category}/{slug}", "why": "raw markdown for one public page" }, + { + "method": "GET", + "path": "/api/docs/search", + "why": "ranked public docs search on newer stacks; the prose is probe-shaped (404 on older stacks falls back to reading the index)" + }, { "method": "GET", "path": "/activate", @@ -64,32 +69,99 @@ } ], "doc_slugs": [ - { "slug": "reference/cli", "served": "public" }, - { "slug": "reference/api", "served": "public" }, - { "slug": "reference/building-apps", "served": "public" }, - { "slug": "how-to/upload-and-import-data", "served": "public" }, - { "slug": "how-to/map-files-into-a-knowledge-graph", "served": "public" }, - { "slug": "concepts/how-data-gets-in", "served": "public" }, - { "slug": "concepts/spaces-tasks-and-agents", "served": "public" }, - { "slug": "how-to/connect-an-iceberg-catalog", "served": "public" }, - { "slug": "how-to/create-a-dataset-from-iceberg", "served": "public" }, - { "slug": "concepts/iceberg-r2rml-mapping", "served": "public" }, - { "slug": "how-to/publish-a-ledger-with-the-cli", "served": "public" }, - { "slug": "how-to/connect-a-remote-fluree", "served": "public" }, - { "slug": "how-to/access-policies", "served": "public" }, - { "slug": "concepts/policies-and-data-governance", "served": "public" }, + { + "slug": "reference/cli", + "served": "public" + }, + { + "slug": "reference/api", + "served": "public" + }, + { + "slug": "reference/building-apps", + "served": "public" + }, + { + "slug": "how-to/upload-and-import-data", + "served": "public" + }, + { + "slug": "how-to/map-files-into-a-knowledge-graph", + "served": "public" + }, + { + "slug": "concepts/how-data-gets-in", + "served": "public" + }, + { + "slug": "concepts/spaces-tasks-and-agents", + "served": "public" + }, + { + "slug": "how-to/connect-an-iceberg-catalog", + "served": "public" + }, + { + "slug": "how-to/create-a-dataset-from-iceberg", + "served": "public" + }, + { + "slug": "concepts/iceberg-r2rml-mapping", + "served": "public" + }, + { + "slug": "how-to/publish-a-ledger-with-the-cli", + "served": "public" + }, + { + "slug": "how-to/connect-a-remote-fluree", + "served": "public" + }, + { + "slug": "how-to/access-policies", + "served": "public" + }, + { + "slug": "concepts/policies-and-data-governance", + "served": "public" + }, { "slug": "how-to/connect-external-mcp-client", - "served": "in-app", - "why": "outside the public allowlist as of solo main — the plugin sends users to the signed-in /docs URL for it. Flip to public (and relink) if solo#1040 B4 widens the allowlist." + "served": "public", + "why": "outside the public allowlist as of solo main \u2014 the plugin sends users to the signed-in /docs URL for it. Flip to public (and relink) if solo#1040 B4 widens the allowlist.", + "note": "public since fluree/solo#1041 widened the allowlist to the full corpus; older stacks 404 this slug and the prose routes users to the signed-in URL" } ], "mcp_tools": [ - { "name": "docs_search", "surface": "stack", "probe_only": true }, - { "name": "docs_get", "surface": "stack", "probe_only": true }, - { "name": "docs_tree", "surface": "stack", "probe_only": true }, - { "name": "docs_examples", "surface": "stack", "probe_only": true }, - { "name": "sparql_query", "surface": "space", "probe_only": true }, - { "name": "get_data_model", "surface": "space", "probe_only": true } + { + "name": "docs_search", + "surface": "stack", + "probe_only": true + }, + { + "name": "docs_get", + "surface": "stack", + "probe_only": true + }, + { + "name": "docs_tree", + "surface": "stack", + "probe_only": true + }, + { + "name": "docs_examples", + "surface": "stack", + "probe_only": true + }, + { + "name": "sparql_query", + "surface": "space", + "probe_only": true + }, + { + "name": "get_data_model", + "surface": "space", + "probe_only": true + } ] } diff --git a/plugins/fluree-companion/skills/fluree-companion/references/stack-knowledge.md b/plugins/fluree-companion/skills/fluree-companion/references/stack-knowledge.md index 4f68ac0..2db23fb 100644 --- a/plugins/fluree-companion/skills/fluree-companion/references/stack-knowledge.md +++ b/plugins/fluree-companion/skills/fluree-companion/references/stack-knowledge.md @@ -36,11 +36,11 @@ So the chain is: **fetch the index → pick slugs by title and description → f curl -fsS https:///api/docs/how-to/upload-and-import-data ``` -There is no search endpoint in this path — the index *is* the search surface, and it is small enough to read in full. Pick from it rather than guessing a slug: fetching an unknown slug returns 404, and a slug that exists on one stack may not exist on another. +Newer stacks also serve ranked search at `GET /api/docs/search?q=…` — and their `/api/docs` index says so in its header. Probe it first; on 404 (an older stack), the index *is* the search surface, and it is small enough to read in full. Either way, pick real slugs rather than guessing: fetching an unknown slug returns 404, and a slug that exists on one stack may not exist on another. ## What the public index leaves out -The `/api/docs` set is narrower than the docs a signed-in user sees in the UI. Pages outside it 404 on `/api/docs/...` while rendering fine at `https:///docs/{category}/{slug}` for a signed-in user. `how-to/connect-external-mcp-client` is one of them today. +On some stack releases the `/api/docs` set is narrower than the docs a signed-in user sees in the UI (newer releases serve the whole corpus publicly). A withheld page 404s on `/api/docs/...` while rendering fine at `https:///docs/{category}/{slug}` for a signed-in user — the MCP/agent how-tos were the commonly withheld ones. So: if a page you expect isn't in the index, that is not evidence the stack lacks the feature. Point the user at the signed-in `/docs/...` URL instead, and say why. diff --git a/plugins/fluree-companion/skills/fluree-companion/references/stories.md b/plugins/fluree-companion/skills/fluree-companion/references/stories.md index f49acc3..b78688c 100644 --- a/plugins/fluree-companion/skills/fluree-companion/references/stories.md +++ b/plugins/fluree-companion/skills/fluree-companion/references/stories.md @@ -21,7 +21,7 @@ Slugs below are `category/slug` on the stack's own docs: `https:///api/do ## Expose a Space over MCP **Goal:** a Space's governed data and tools become an MCP server that Claude Code (or Claude Desktop, or claude.ai) can use. -**Knowledge:** `how-to/connect-external-mcp-client` — **not in the public `/api/docs` set today**, so send the user to the signed-in `https:///docs/how-to/connect-external-mcp-client`. `concepts/spaces-tasks-and-agents` for what a Space is. +**Knowledge:** `how-to/connect-external-mcp-client` — if the public `/api/docs` fetch 404s (older stacks withhold this page), send the user to the signed-in `https:///docs/how-to/connect-external-mcp-client`. `concepts/spaces-tasks-and-agents` for what a Space is. **Entry points:** the Space's **MCP** tab (owner-only: enable access, choose exposed tools, read off the server URL and `spaceId`), then `claude mcp add --transport http fluree-space "https:///v1/mcp?space="`. ## Connect an Iceberg catalog