From 56735a52900c072da1c955086752743a0bd5f989 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 7 May 2026 17:58:04 +0100 Subject: [PATCH] docs: skills + revisions + proposals reference, plus cheatsheet update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 7 of the Skills + Revisions + Proposals work — documentation pass for the surface added in PR-1 through PR-6. Reference material only; no code changes. ## What's added - `docs/skills.md` — skill model, scoping rules, CLI surface, the `mcpctl config claude --project` setup flow, metadata schema (with the deferred-execution note for hooks/mcpServers/postInstall), the on-disk state file shape, atomic install mechanics, failure semantics, and what's deferred. - `docs/revisions.md` — ResourceRevision model, semver auto-bump rules, contentHash diff key (cross-resource sync), CLI for history / diff / restore, RBAC, audit emission, storage growth note. - `docs/proposals.md` — ResourceProposal model, the reviewer flow (CLI + web UI), atomic-approval mechanics, the propose_prompt / propose_skill MCP tools, the propose-learnings global skill that steers Claude toward engaging with them, and the deferred legacy PromptRequest cutover. ## What's edited - Top-level `CLAUDE.md` — resource cheatsheet adds `skill`, `proposal`, `revision` with cross-references to the new docs. The legacy `promptrequest` entry stays (still on the legacy code path) but notes that new work should use `proposal`. ## What's NOT in this PR - The PromptRequest → ResourceProposal cutover migration. Both run side-by-side today; the focused cutover PR will rename + backfill + drop. Keeping that out of PR-7 means review can stay on docs. - Bundle-backup / `mcpctl apply -f` skill support (deferred from PR-3). - `metadata.hooks` / `metadata.mcpServers` / `metadata.postInstall` execution (deferred from PR-5). - Existing-page UI migration to Tailwind (deferred from PR-6 — old inline-styled pages coexist fine inside the new Layout). These are tracked as future PRs; each is its own focused change. ## Verification `pnpm test:run` whole monorepo: 162 test files / 2157 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 5 +- docs/proposals.md | 126 +++++++++++++++++++++++++++ docs/revisions.md | 130 ++++++++++++++++++++++++++++ docs/skills.md | 214 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 docs/proposals.md create mode 100644 docs/revisions.md create mode 100644 docs/skills.md diff --git a/CLAUDE.md b/CLAUDE.md index 967d913..c7fd973 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,9 @@ Key routing rules: - `project` — workspace grouping servers, prompts, agents - `llm` — server-managed LLM provider (api key + endpoint) - `agent` — LLM persona pinned to one Llm; project attach surfaces project Prompts as system context, project MCP servers as tools, and exposes the agent itself as an MCP virtual server (`agent-/chat`). See `docs/agents.md`, `docs/chat.md`. -- `prompt` / `promptrequest` — curated content / pending proposal +- `prompt` / `promptrequest` — curated content / legacy pending proposal (use `proposal` for new work). +- `skill` — Claude Code skill bundle (SKILL.md + files + typed metadata). Materialised onto disk by `mcpctl skills sync`. See `docs/skills.md`. +- `proposal` — generic pending proposal queue, replaces `promptrequest`. Covers both prompts and skills. See `docs/proposals.md`. Triage via `mcpctl review`. +- `revision` — append-only audit + diff log shared by prompts and skills. Auto-bumps semver on save. See `docs/revisions.md`. - `rbac` — access control bindings - `mcptoken` — bearer credentials for HTTP-mode mcplocal diff --git a/docs/proposals.md b/docs/proposals.md new file mode 100644 index 0000000..02147be --- /dev/null +++ b/docs/proposals.md @@ -0,0 +1,126 @@ +# Resource Proposals + +A proposal is a pending change to a Prompt or Skill, submitted by +either a Claude Code session (via the `propose_prompt` / `propose_skill` +MCP tools) or a human (via the web UI / CLI). Reviewers triage the +queue and either approve — at which point the proposal becomes a real +prompt or skill — or reject with a note. + +This is the path by which Claude **proposes back** to mcpd: things the +session learned that future sessions would benefit from. The +`propose-learnings` global skill (seeded by mcpd at startup) explains +the discipline to Claude. + +## Model + +`ResourceProposal` shares the schema's discriminator pattern with +`ResourceRevision` — single table, `resourceType` field disambiguates +prompts vs skills. + +| Field | Purpose | +|----------------------|--------------------------------------------------------| +| `resourceType` | `'prompt'` \| `'skill'`. | +| `name` | Proposed resource name. | +| `body` | Proposed body (`{ content, priority?, metadata?, … }`).| +| `projectId` / `agentId` | Scope of the proposal (XOR; null/null = global). | +| `createdBySession` | mcplocal session that proposed (when from Claude). | +| `createdByUserId` | User who proposed (when via UI/CLI). | +| `status` | `'pending'` → `'approved'` \| `'rejected'`. | +| `reviewerNote` | Set on approval or rejection. | +| `approvedRevisionId` | Set when approved — points at the resulting revision. | + +Two unique constraints — `(resourceType, name, projectId)` and +`(resourceType, name, agentId)` — mirror the Prompt / Skill scoping +rules. The same `?? ''` workaround for nullable-FK lookups applies. + +## Reviewer flow + +### CLI + +```bash +mcpctl review pending # list pending +mcpctl review next # show oldest pending +mcpctl review show # full detail +mcpctl review diff # before/after diff +mcpctl review approve # POST /proposals/:id/approve +mcpctl review reject --reason "explain" # rejected with note +``` + +### Web UI + +`/proposals` shows a Pending / Approved / Rejected tab view; the +sidebar nav badge polls every 30 s and shows the pending count in +amber. Click a row to see the full body, the diff against the current +resource (if any), and approve / reject controls. + +### Approval is atomic + +Approval runs in a single Prisma transaction: + +1. Look up the pending proposal. +2. Dispatch by `resourceType` to the registered handler + (`PromptService` or `SkillService` registers itself at construction). +3. Handler upserts the underlying resource — creating it if new, or + updating + auto-bumping patch semver if it exists. +4. Handler records a `ResourceRevision` linking back to the proposal. +5. Proposal status flips to `approved`, `approvedRevisionId` set. + +If any step fails, the transaction rolls back and the proposal stays +`pending`. There is no half-approved state. + +## Claude side: `propose_prompt` and `propose_skill` + +Both tools are registered by the `gate` plugin in mcplocal. They post +to `/api/v1/proposals` with the appropriate `resourceType`. + +The `propose-learnings` global skill (seeded by mcpd) tells Claude +*when* to use them: + +- `propose_prompt` for project-specific knowledge — gotchas, + conventions, hidden constraints. Cheap to add, easy to reject. +- `propose_skill` for cross-cutting knowledge — debugging discipline, + release hygiene, security review style. Larger blast radius; lean + toward `propose_prompt` unless you have a clear cross-project reason. + +The `gate-encouragement-propose` system prompt (priority 10, sits in +the gating bundle) is the trigger that makes Claude actually consider +proposing. Without that, the tools exist but Claude rarely engages. + +## Backwards compat + +PR-1 / PR-2 deferred the cutover from the prompt-only `PromptRequest` +table to `ResourceProposal`. Both run side-by-side today: + +- mcplocal's `propose_prompt` still POSTs to the legacy + `/api/v1/projects/:name/promptrequests` URL. +- mcplocal's `propose_skill` (newer) POSTs to `/api/v1/proposals` + directly. +- The legacy `/api/v1/promptrequests*` routes remain in mcpd. +- `mcpctl approve promptrequest ` still works. + +A focused follow-up PR will: + +1. Migrate existing `PromptRequest` rows into `ResourceProposal` + (resourceType=prompt). +2. Rename `PromptRequest` to `_PromptRequest_legacy`. +3. Update mcplocal's `propose_prompt` to use `/api/v1/proposals`. +4. Keep the legacy URL as a thin translation shim through one release. +5. Drop `_PromptRequest_legacy` after that. + +This stays separate so the cutover is reviewable independently of +the larger Skills + Revisions + Proposals work. + +## RBAC + +Proposals piggyback on the `prompts` permission for now — anyone with +`view:prompts` can read the queue, anyone with `edit:prompts` can +approve or reject. Splitting out a dedicated `proposals` permission +(or a "reviewer" role) is straightforward if granularity becomes +useful. + +## Audit emission + +Proposal create / approve / reject events flow through the existing +audit pipeline. Approval events also reference the resulting +revision id, so you can join "proposal approved at T" against +"revision X created at T" without polling. diff --git a/docs/revisions.md b/docs/revisions.md new file mode 100644 index 0000000..4463af0 --- /dev/null +++ b/docs/revisions.md @@ -0,0 +1,130 @@ +# Resource Revisions + +mcpctl keeps an append-only revision log for every Prompt and Skill — +so you can answer "who changed prompt X and when," diff between any +two versions, and restore an earlier state without losing the audit +chain. + +## Model + +`ResourceRevision` is a single shared table keyed by +`(resourceType, resourceId)` — the type discriminator allows the same +infrastructure to cover both prompts and skills (and any future +resource that wants version history). + +| Field | Purpose | +|------------------|----------------------------------------------------------| +| `id` | cuid; the revision's stable identity. | +| `resourceType` | `'prompt'` \| `'skill'`. Validated app-layer. | +| `resourceId` | Soft FK — survives deletion of the underlying resource. | +| `semver` | Author-visible version (X.Y.Z). | +| `contentHash` | sha256 of the canonicalised body. Stable diff key. | +| `body` | Snapshot of the resource at this revision. | +| `authorUserId` | Who made the change (null for system writes). | +| `authorSessionId`| Session that proposed it (when applicable). | +| `note` | Free-text reviewer or author note. | +| `createdAt` | When the revision was recorded. | + +The resource row itself (Prompt/Skill) keeps the inline `content` — +revisions are an audit log, not the source of truth. Hot read paths +(the gate plugin, `mcpctl skills sync`, prompt indexing) never need +to consult the revision log. + +`Prompt.currentRevisionId` and `Skill.currentRevisionId` are soft +pointers to the latest revision so the UI can answer "which version is +live" in one query. + +## Semver semantics + +Auto-patch on every successful save where the body changed: + +``` +0.1.0 → save with content change → 0.1.1 +0.1.1 → save with content change → 0.1.2 +``` + +Authors can override: + +```bash +mcpctl edit prompt foo --bump minor # 0.1.x → 0.2.0 +mcpctl edit prompt foo --bump major # 0.x.x → 1.0.0 +mcpctl edit prompt foo --semver 1.2.3 # explicit +mcpctl edit prompt foo --note "fixed the gotcha" # adds note to revision +``` + +Invalid semver values fall back to `0.1.0` rather than throwing — +the revision write is best-effort and we don't want a corrupted +existing semver to break the prompt save. + +## contentHash + +sha256 of the JSON-canonicalised body (keys sorted at every object +level). Two revisions with the same hash are byte-identical. Used by +`mcpctl skills sync` as the diff key against on-disk state — re-publish +under the same semver still triggers a sync if the contentHash changed. + +The server-side hash and the client-side hash are computed from the +same canonical shape, so they match exactly. See +`src/mcpd/src/services/resource-revision.service.ts` for the canonical +JSON encoder. + +## CLI + +### View history + +```bash +mcpctl get revisions prompt my-prompt +mcpctl get revisions skill demo-skill +``` + +### View one + +```bash +mcpctl describe revision +``` + +### Diff + +The HTTP API returns a unified-format diff: + +``` +GET /api/v1/revisions//diff?against= +``` + +The web UI's revision history tab on a Skill detail page renders the +diff inline (color-coded add/remove rows). + +### Restore + +Restore a prompt or skill to an earlier revision. This writes a *new* +revision whose body is the old one — preserving the audit chain +rather than deleting later revisions. + +```bash +mcpctl restore prompt my-prompt --revision +``` + +The CLI subcommand is wired through to `POST +/api/v1/prompts/:id/restore-revision` (and the symmetric +`/api/v1/skills/:id/restore-revision`). + +## RBAC + +Revisions piggyback on the underlying resource's RBAC permission. If +you can `view:prompts`, you can read prompt history; if you can +`edit:prompts`, you can restore. + +## Audit emission + +Each revision write emits a structured audit event captured by the +existing audit-event pipeline. The event includes the revision id, +contentHash, semver, and author/session — sufficient to answer "what +changed" and "who" without joining tables manually. + +## Storage size + +A revision body is the resource snapshot — for prompts that's a few +KB; for skills with large `files` maps it can be tens of KB. The audit +log grows linearly with edits. v1 has no rotation; if a single resource +sees thousands of revisions per day this will need a retention policy +(out of scope today). diff --git a/docs/skills.md b/docs/skills.md new file mode 100644 index 0000000..5796afd --- /dev/null +++ b/docs/skills.md @@ -0,0 +1,214 @@ +# Skills + +Skills are Claude Code skill bundles distributed by mcpctl. Each skill is a +named bundle of files — at minimum a `SKILL.md` explaining the skill's purpose +and triggers, optionally with auxiliary scripts, templates, or data files. The +mcpctl daemon (mcpd) is the source of truth; `mcpctl skills sync` materialises +the skills onto each dev machine under `~/.claude/skills//`, where Claude +Code reads them natively. + +``` +┌─ mcpd (Postgres) ──────────────────────────────┐ +│ Skill rows (content + files{} + metadata) │ +└────────────────┬───────────────────────────────┘ + │ HTTP, hash-pinned diff + ▼ +┌─ ~/.claude/skills// ─────────────────────┐ +│ SKILL.md │ +│ scripts/setup.sh │ +│ … │ +└────────────────────────────────────────────────┘ +``` + +## Trust model + +Skills are added by senior admins together with a security reviewer at +publish time on mcpd. Once content is in mcpd, clients trust what mcpd +serves — no client-side sandboxing, no signature checks, no consent +prompts. The rigor lives on the publishing side (RBAC, audit, the +reviewer queue). See [proposals.md](proposals.md) for the +review→approve flow. + +If you're publishing skills to clients you don't trust (e.g. an open- +source distribution), the design is wrong for that — the skill format +itself is fine, but the unguarded client trust assumption isn't. + +## Scoping + +A skill attaches to one of: + +- **Global** — `projectId` and `agentId` both null. Synced onto every dev + machine when its sync runs (with or without a project context). +- **Project-scoped** — `projectId` set. Synced onto machines whose + `.mcpctl-project` marker matches. +- **Agent-scoped** — `agentId` set. Surfaced administratively via the + API; not currently materialised onto disk by `mcpctl skills sync` + (see "Future" below). + +The same `` can exist at multiple scopes simultaneously. The two +unique constraints are `(name, projectId)` and `(name, agentId)`. + +## CLI + +### Create + +```bash +mcpctl create skill \ + [--project | --agent ] \ + --content / --content-file \ + [--description ""] \ + [--priority <1-10>] \ + [--semver ] \ + [--metadata-file ] \ + [--files-dir ] +``` + +`--content-file` provides the `SKILL.md` body. `--metadata-file` +accepts YAML or JSON; see "Metadata" below for the schema. `--files-dir` +walks a directory tree into the `files{}` map (UTF-8 only; non-text +files rejected — extend later if needed). + +### Edit + +```bash +# Edit content in $EDITOR +mcpctl edit skill + +# Edit + bump semver +mcpctl edit skill --bump major|minor|patch --note "" + +# Edit + set explicit semver +mcpctl edit skill --semver 1.2.3 +``` + +Each save records a `ResourceRevision` automatically. See +[revisions.md](revisions.md). + +### Sync to disk + +```bash +# In a project directory (with .mcpctl-project marker): +mcpctl skills sync + +# Override project: +mcpctl skills sync --project + +# Globals only (no project context, no marker): +cd / && mcpctl skills sync + +# Used by the SessionStart hook — fail-open on network errors: +mcpctl skills sync --quiet +``` + +Useful flags: + +| Flag | Purpose | +|---------------------|-----------------------------------------------------------| +| `--dry-run` | Print what would change, don't write anything. | +| `--force` | Overwrite locally-modified skills. | +| `--quiet` | Suppress output unless something changed; fail-open. | +| `--keep-orphans` | Don't remove skills no longer in the server set. | +| `--skip-postinstall`| Reserved for the postInstall executor (deferred). | + +## Project setup + +`mcpctl config claude --project ` does the full pickup chain: + +1. Writes `.mcp.json` so Claude Code routes MCP traffic through mcplocal. +2. Writes `.mcpctl-project` (single line, project name) so `skills sync` + knows which project's skills to pull when run from anywhere under + that directory. +3. Runs an initial `skills sync` synchronously. +4. Installs a SessionStart hook in `~/.claude/settings.json` that runs + `mcpctl skills sync --quiet` before every Claude session. Tagged + with `_mcpctl_managed: true` so subsequent runs find and update it + instead of duplicating it. + +Pass `--skip-skills` to opt out of steps 2–4 (useful in CI). + +## Metadata + +The `metadata` field is a typed JSON blob: + +```yaml +hooks: + PreToolUse: + - type: command + command: "echo before-tool" + PostToolUse: + - type: command + command: "echo after-tool" + SessionStart: + - type: command + command: "echo session-started" +mcpServers: + - name: my-grafana + fromTemplate: grafana + project: monitoring +postInstall: scripts/install.sh +preUninstall: scripts/cleanup.sh +postInstallTimeoutSec: 60 +``` + +**v1 sync executes none of these — they're stored verbatim and +materialisation is deferred to a follow-up.** Once enabled: + +- `hooks` will be written into `~/.claude/settings.json` with + `_mcpctl_managed: true` markers (see Project Setup above for how + the SessionStart hook works today). +- `mcpServers` will be auto-attached via the mcpd attach API. +- `postInstall` will run as the user with a curated env, hard timeout, + and an audit event emitted back to mcpd. Hash-pinned: re-syncs of + unchanged scripts won't re-execute. + +## State + +`~/.mcpctl/skills-state.json` tracks the last-synced state: + +- per-skill: `id`, `semver`, `contentHash` (matches mcpd's hash), + `installDir`, per-file `sha256` + size, `postInstallHash`, + `lastSyncedAt`. +- top-level: `lastSync`, `lastSyncProject`, `schemaVersion`. + +The state file is written atomically (temp + rename). Per-file SHA-256 +detects local edits — sync warns and skips modified files unless you +pass `--force`. + +State lives outside `~/.claude/skills/` deliberately so Claude Code +doesn't see our bookkeeping in its tree. + +## Atomic install + +Each skill is staged under `.mcpctl-staging-/`, then +the existing directory (if any) is renamed to +`.mcpctl-trash-`, the staging dir is moved into place, +and the trash is rmtree'd. A concurrent reader (Claude Code starting up) +never sees a partial tree. + +Symmetric atomic delete for orphan removal: rename to trash, rmtree. +Locally-modified skills are preserved (warned + skipped) unless `--force`. + +## Failure semantics + +| Situation | Exit code | Behaviour | +|----------------------------------|-----------|------------------------------------| +| Network/timeout in `--quiet` | 0 | Skip silently. SessionStart hook never blocks Claude. | +| Auth failure | 1 | "run mcpctl login" message. | +| Disk full / state save failure | 2 | Loud error. | +| Per-skill error | 0 | Logged in result errors[]; sync continues. | + +The fail-open behaviour in `--quiet` is non-negotiable — a hung mcpd +must never block Claude Code starting up. + +## Future + +The following are deferred to follow-up PRs: + +- `metadata.hooks` materialisation into `~/.claude/settings.json` +- `metadata.mcpServers` auto-attach +- `metadata.postInstall` execution with curated env + audit emission +- Agent-scoped skills synced to disk (would need an agent-identity-on- + disk concept that doesn't exist yet) +- Bundle backup support for skills (bundle-backup is one path; git-backup + is the other and is wired today) +- `mcpctl apply -f skill.yaml` declarative skill apply