docs: skills + revisions + proposals reference, plus cheatsheet update
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) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,9 @@ Key routing rules:
|
|||||||
- `project` — workspace grouping servers, prompts, agents
|
- `project` — workspace grouping servers, prompts, agents
|
||||||
- `llm` — server-managed LLM provider (api key + endpoint)
|
- `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-<name>/chat`). See `docs/agents.md`, `docs/chat.md`.
|
- `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-<name>/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
|
- `rbac` — access control bindings
|
||||||
- `mcptoken` — bearer credentials for HTTP-mode mcplocal
|
- `mcptoken` — bearer credentials for HTTP-mode mcplocal
|
||||||
|
|||||||
126
docs/proposals.md
Normal file
126
docs/proposals.md
Normal file
@@ -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 <id> # full detail
|
||||||
|
mcpctl review diff <id> # before/after diff
|
||||||
|
mcpctl review approve <id> # POST /proposals/:id/approve
|
||||||
|
mcpctl review reject <id> --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 <name>` 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.
|
||||||
130
docs/revisions.md
Normal file
130
docs/revisions.md
Normal file
@@ -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 <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
The HTTP API returns a unified-format diff:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/revisions/<id>/diff?against=<other-id|live>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <revision-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
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).
|
||||||
214
docs/skills.md
Normal file
214
docs/skills.md
Normal file
@@ -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/<name>/`, where Claude
|
||||||
|
Code reads them natively.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ mcpd (Postgres) ──────────────────────────────┐
|
||||||
|
│ Skill rows (content + files{} + metadata) │
|
||||||
|
└────────────────┬───────────────────────────────┘
|
||||||
|
│ HTTP, hash-pinned diff
|
||||||
|
▼
|
||||||
|
┌─ ~/.claude/skills/<name>/ ─────────────────────┐
|
||||||
|
│ 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 `<name>` can exist at multiple scopes simultaneously. The two
|
||||||
|
unique constraints are `(name, projectId)` and `(name, agentId)`.
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
### Create
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcpctl create skill <name> \
|
||||||
|
[--project <name> | --agent <name>] \
|
||||||
|
--content / --content-file <path> \
|
||||||
|
[--description "<text>"] \
|
||||||
|
[--priority <1-10>] \
|
||||||
|
[--semver <X.Y.Z>] \
|
||||||
|
[--metadata-file <path>] \
|
||||||
|
[--files-dir <path>]
|
||||||
|
```
|
||||||
|
|
||||||
|
`--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 <name>
|
||||||
|
|
||||||
|
# Edit + bump semver
|
||||||
|
mcpctl edit skill <name> --bump major|minor|patch --note "<message>"
|
||||||
|
|
||||||
|
# Edit + set explicit semver
|
||||||
|
mcpctl edit skill <name> --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 <name>
|
||||||
|
|
||||||
|
# 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 <name>` 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 `<targetDir>.mcpctl-staging-<pid>/`, then
|
||||||
|
the existing directory (if any) is renamed to
|
||||||
|
`<targetDir>.mcpctl-trash-<pid>`, 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
|
||||||
Reference in New Issue
Block a user