From bd1b1cdba3eb2dcc5be3614eb97365667aafc74a Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:03:22 -0500 Subject: [PATCH 01/11] docs: add MCP server deduplication design document Design for installer-level MCP server deduplication to address Issue #8. Key decisions: - Identity model: key_name + origin tuple - Lockfile: new shared_mcp_servers section - Highest version wins by default - User override via --no-dedup or per-server config - Re-extract from cached archives on uninstall - No manifest or archive format changes --- docs/plans/2026-02-15-mcp-dedup-design.md | 188 ++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/plans/2026-02-15-mcp-dedup-design.md diff --git a/docs/plans/2026-02-15-mcp-dedup-design.md b/docs/plans/2026-02-15-mcp-dedup-design.md new file mode 100644 index 0000000..5a438aa --- /dev/null +++ b/docs/plans/2026-02-15-mcp-dedup-design.md @@ -0,0 +1,188 @@ +# MCP Server Deduplication Design + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Prevent duplicate MCP server processes, config bloat, and context waste when multiple ccpkg packages bundle the same MCP server. + +**Architecture:** Installer-level deduplication using a two-part identity model (key name + origin). Packages remain self-contained. No manifest or archive format changes. The installer is smarter about what it writes to the host's MCP config. + +**Scope:** Specification text, lockfile schema, install/uninstall lifecycle. No tooling implementation (that is a separate effort). + +--- + +## Problem Statement + +The ccpkg spec requires packages to be self-contained (Principle #1) with no inter-package dependencies (Principle #8). When multiple packages bundle the same MCP server, this causes: + +1. **Duplicate MCP processes** -- Multiple instances of the same server run simultaneously, wasting resources and causing port/socket conflicts. +2. **Config file bloat** -- The host's `.mcp.json` gets polluted with redundant server entries from each plugin. +3. **Context/token waste** -- Each plugin loads its own copy of MCP server metadata and tool descriptions, consuming context window space unnecessarily. + +The problem is systemic and worsens as the plugin ecosystem grows. + +## Design Decision: Installer-Level Deduplication + +Packages remain fully self-contained. Authors continue to bundle MCP servers as they do today. The installer detects duplicates at install time and resolves them before writing to disk. + +### Alternatives Considered + +**A. Shared MCP directory with reference counting** -- Promotes shared servers to `~/.ccpkg/shared-mcp/`. Adds a new directory layout concept and reference counting complexity. Breaks the "each plugin is self-contained in its directory" model. Rejected: too much structural change. + +**B. MCP server identity field in manifest** -- Adds a `server_id` field to MCP configs for explicit identity declaration. Most precise but requires schema change and author adoption. Does not help existing packages. Rejected: adoption barrier. + +**C. Install-time dedup with re-extract (selected)** -- No schema changes. Transparent to authors. Re-extract on uninstall avoids reference counting. Works retroactively with existing packages. + +--- + +## MCP Server Identity Model + +An MCP server's identity is a tuple of **(key_name, origin)**: + +- **key_name**: The key in the `mcpServers` object (e.g., `"context7"`, `"github"`) +- **origin**: Derived from the server mode: + - Mode 1 (command+args): `command::{command} {args[0]}` (e.g., `command::npx -y @anthropic/context7-mcp`) + - Mode 2 (embedded mcpb): `bundle::{bundle_path}` normalized + - Mode 3 (referenced mcpb): the `source` URL verbatim + +Two MCP servers are considered **the same server** when both key_name and origin match. + +### Version Resolution + +Version is extracted from the origin where possible (npm package version, URL path segment, mcpb metadata). + +- **Same identity, incoming version higher**: replace with incoming. Re-render MCP config. +- **Same identity, incoming version equal or lower**: skip rendering. Track in lockfile only. +- **Same key_name, different origin**: conflict. Warn user and prompt. + +--- + +## Lockfile Changes + +A new top-level `shared_mcp_servers` section in `ccpkg-lock.json`: + +```json +{ + "shared_mcp_servers": { + "context7": { + "origin": "command::npx -y @anthropic/context7-mcp", + "version": "1.3.0", + "declared_by": ["plugin-a", "plugin-b", "plugin-c"], + "active_source": "plugin-b", + "dedup": true, + "installed_at": "2026-02-15T12:00:00Z" + } + } +} +``` + +Fields: + +| Field | Type | Description | +|---|---|---| +| `origin` | string | Identity origin string derived from server mode | +| `version` | string or null | Resolved winning version, null if unversioned | +| `declared_by` | string[] | Package names that bundle this server | +| `active_source` | string | Package whose copy is currently installed | +| `dedup` | boolean | Whether dedup is active for this server (default: true) | +| `installed_at` | string | ISO 8601 timestamp of last resolution | + +--- + +## Install Flow Changes + +Current step 10 ("Render templates") becomes: + +**Step 10 (revised): Render and deduplicate MCP servers.** + +1. Parse the incoming package's `.mcp.json` template. +2. For each server entry, compute its identity tuple (key_name, origin). +3. Check `shared_mcp_servers` in the lockfile: + - **No match**: New server. Render template, merge into host config, add to `shared_mcp_servers` with `declared_by: [this-package]`. + - **Match, incoming version higher**: Re-render using the incoming package's template and config values. Update `active_source` and `version`. Append package to `declared_by`. + - **Match, incoming version equal or lower**: Skip rendering. Append package to `declared_by` only. + - **Key collision, different origin**: Warn user. Offer to keep existing, replace, or install both (rename incoming key with package prefix, e.g., `plugin-b::context7`). +4. Write the deduplicated result to host config. + +--- + +## Uninstall Flow Changes + +Current step 3 ("Remove merged MCP servers") becomes: + +**Step 3 (revised): Remove or reassign MCP servers.** + +1. For each MCP server the package declared, check `shared_mcp_servers`: + - **Package is the only entry in `declared_by`**: Remove the server from host config and from `shared_mcp_servers`. + - **Other packages remain in `declared_by`**: + - Remove this package from `declared_by`. + - If this package was `active_source`: pick the next package in `declared_by` with the highest version, re-extract its MCP template from the cached archive, re-render with that package's config values, update `active_source`. + - If this package was NOT `active_source`: remove from `declared_by` only, no config change. + - **Server has `dedup: false`**: Remove only this package's copy. Other packages' copies are independent. + +--- + +## User Override + +Dedup is the default behavior. Users can override it: + +### Global override + +`--no-dedup` flag on install bypasses all MCP deduplication. + +### Per-server override + +The `dedup` field in `shared_mcp_servers` defaults to `true`. Users can set it to `false` via: + +``` +ccpkg config set dedup.context7 false +``` + +When `dedup` is false for a server, each package gets its own independent copy. + +### Interactive prompt + +When the installer detects a duplicate in interactive mode: + +``` +MCP server "context7" already installed (v1.3.0 from plugin-b). +Package "plugin-c" bundles v1.2.0. + [S]kip (keep v1.3.0) | [D]uplicate (install both) | [R]eplace +``` + +Default (non-interactive/CI): Skip (highest version wins). + +--- + +## Archive Cache + +For re-extract to work on uninstall, the installer MUST cache installed `.ccpkg` archives. + +- **Location**: `~/.ccpkg/cache/archives/{name}-{version}.ccpkg` +- **Retention**: Archives for packages still in any `declared_by` list MUST be retained. +- **Eviction**: Archives for packages no longer referenced by any `declared_by` list MAY be evicted. + +This formalizes caching behavior already implied by Mode 3 (referenced mcpb). + +--- + +## Specification Changes Summary + +| Section | Change | +|---|---| +| Design Principles | Add principle #7: MCP server deduplication | +| MCP Servers | Add "Server Deduplication" subsection after Variable Substitution | +| Lockfile Format | Document `shared_mcp_servers` top-level field | +| Install Lifecycle, step 10 | Revise to include dedup logic | +| Uninstall Lifecycle, step 3 | Revise to include reassignment logic | +| New section: Archive Cache | Formalize cache location and retention rules | + +**No manifest schema changes. No archive format changes.** + +--- + +## Out of Scope + +- CLI implementation of `ccpkg config set dedup.*` -- that is tooling, not spec +- LSP server deduplication -- same pattern could apply but is deferred +- Inter-package dependency resolution -- still explicitly out of scope +- Registry-level dedup metadata -- future optimization From 17d885ddc54bd747daf93fd9f2d9e698eb6056d7 Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:07:10 -0500 Subject: [PATCH 02/11] docs: add MCP server deduplication implementation plan 8-task plan covering design principles, server identity model, lockfile schema, install/uninstall lifecycle changes, archive cache, and design rationale updates. --- docs/plans/2026-02-15-mcp-dedup-plan.md | 391 ++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 docs/plans/2026-02-15-mcp-dedup-plan.md diff --git a/docs/plans/2026-02-15-mcp-dedup-plan.md b/docs/plans/2026-02-15-mcp-dedup-plan.md new file mode 100644 index 0000000..e6f97dd --- /dev/null +++ b/docs/plans/2026-02-15-mcp-dedup-plan.md @@ -0,0 +1,391 @@ +# MCP Server Deduplication Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add MCP server deduplication to the ccpkg specification so installers can prevent duplicate processes, config bloat, and context waste when multiple packages bundle the same MCP server. + +**Architecture:** Six spec sections are modified or added. No manifest schema or archive format changes. Changes touch `spec/specification.md` (source of truth), website MDX files (split pages), and the design rationale doc. + +**Tech Stack:** Markdown (spec), MDX (Astro Starlight website), JSON (lockfile examples) + +--- + +### Task 1: Add Design Principle #7 + +Add MCP server deduplication as a design principle in the spec and website overview. + +**Files:** +- Modify: `spec/specification.md:58-60` (between principle #6 and #8) +- Modify: `src/content/docs/specification/overview.mdx` (matching section) + +**Step 1: Add principle #7 to spec/specification.md** + +After line 58 (principle #6, "No install-time code execution"), before the current principle #8 ("No inter-package dependencies"), insert: + +```markdown +7. **MCP server deduplication.** When multiple packages declare MCP servers with the same identity (key name and origin), the installer SHOULD deduplicate them. The highest version wins by default. Users MUST be able to override deduplication per-server or globally. +``` + +Renumber current #8 to #8 (it stays the same number since we're inserting #7 between #6 and #8). + +**Step 2: Mirror the change in overview.mdx** + +Add the same principle #7 text to `src/content/docs/specification/overview.mdx` in the matching Design Principles section, between principles #6 and #8. + +**Step 3: Verify website builds** + +Run: `npm run build` +Expected: Build succeeds with 0 errors. + +**Step 4: Commit** + +```bash +git add spec/specification.md src/content/docs/specification/overview.mdx +git commit -m "docs(spec): add design principle #7 for MCP server deduplication" +``` + +--- + +### Task 2: Add Server Deduplication Subsection to MCP Servers + +Add the identity model, version resolution, and user override spec language to the MCP Servers section. + +**Files:** +- Modify: `spec/specification.md:798` (after Variable Substitution, before LSP Servers) +- Modify: `src/content/docs/specification/component-types.mdx` (matching section) + +**Step 1: Add Server Deduplication subsection to spec** + +After the Variable Substitution bullet list (line ~798) and before `### LSP Servers` (line ~800), insert: + +```markdown +**Server Deduplication:** + +When installing a package that declares an MCP server already present in the host configuration, the installer SHOULD deduplicate rather than creating a duplicate entry. + +**Server Identity.** An MCP server's identity is a tuple of (key_name, origin): + +- **key_name**: The key in the `mcpServers` object (e.g., `"context7"`). +- **origin**: Derived from the server mode: + - Mode 1 (command+args): `command::{command} {args[0]}` (e.g., `command::npx -y @anthropic/context7-mcp`). + - Mode 2 (embedded mcpb): `bundle::{bundle_path}` normalized to the archive-relative path. + - Mode 3 (referenced mcpb): the `source` URL verbatim. + +Two servers are considered the same when both key_name and origin match. + +**Version Resolution.** Version is extracted from the origin where possible (npm package version, URL path segment, mcpb metadata). + +- Same identity, incoming version higher: replace. Re-render MCP config from the incoming package's template. +- Same identity, incoming version equal or lower: skip rendering. Track in lockfile only. +- Same key_name, different origin: conflict. The installer MUST warn the user. In interactive mode, the installer SHOULD offer to keep the existing server, replace it, or install both under distinct keys. + +**User Override.** Deduplication is the default behavior. Installers MUST provide a mechanism for users to override deduplication: + +- A global flag (e.g., `--no-dedup`) that bypasses all MCP deduplication for the current install operation. +- A per-server override stored in the lockfile's `shared_mcp_servers` entry (`"dedup": false`). When dedup is disabled for a server, each package gets its own independent copy. +``` + +**Step 2: Mirror the change in component-types.mdx** + +Add the same Server Deduplication content to `src/content/docs/specification/component-types.mdx` in the MCP Servers section, after Variable Substitution. + +**Step 3: Verify website builds** + +Run: `npm run build` +Expected: Build succeeds. + +**Step 4: Commit** + +```bash +git add spec/specification.md src/content/docs/specification/component-types.mdx +git commit -m "docs(spec): add MCP server deduplication identity model and rules" +``` + +--- + +### Task 3: Add shared_mcp_servers to Lockfile Format + +Document the new top-level lockfile field for tracking shared MCP servers. + +**Files:** +- Modify: `spec/specification.md:1229-1230` (after lockfile schema JSON block closing brace, before field tables) +- Modify: `src/content/docs/specification/lockfile.mdx` (matching section) + +**Step 1: Extend lockfile schema example in spec** + +In the lockfile JSON example, add `shared_mcp_servers` as a top-level sibling of `packages`: + +```json + "shared_mcp_servers": { + "context7": { + "origin": "command::npx -y @anthropic/context7-mcp", + "version": "1.3.0", + "declared_by": ["plugin-a", "plugin-b"], + "active_source": "plugin-b", + "dedup": true, + "installed_at": "2026-02-15T12:00:00Z" + } + } +``` + +**Step 2: Add field documentation table after the existing lockfile field tables** + +After the "Remote source entry fields" table (line ~1266), add: + +```markdown +**Shared MCP server fields:** + +The `shared_mcp_servers` top-level field tracks MCP servers that are declared by multiple packages. Keys are MCP server key names. + +| Field | Type | Description | +|---|---|---| +| `origin` | `string` | Identity origin string derived from server mode (see [Server Deduplication](#server-deduplication)). | +| `version` | `string \| null` | Resolved winning version. `null` if the server version cannot be determined. | +| `declared_by` | `string[]` | Package names that bundle this server. | +| `active_source` | `string` | Name of the package whose MCP template is currently rendered in the host config. | +| `dedup` | `boolean` | Whether deduplication is active for this server. Defaults to `true`. When `false`, each package installs its own independent copy. | +| `installed_at` | `string` | ISO 8601 timestamp of the last resolution event. | +``` + +**Step 3: Mirror in lockfile.mdx** + +Add the same schema example and field table to `src/content/docs/specification/lockfile.mdx`. + +**Step 4: Verify website builds** + +Run: `npm run build` +Expected: Build succeeds. + +**Step 5: Commit** + +```bash +git add spec/specification.md src/content/docs/specification/lockfile.mdx +git commit -m "docs(spec): add shared_mcp_servers lockfile field for dedup tracking" +``` + +--- + +### Task 4: Revise Install Lifecycle Step 10 + +Update the install sequence to include MCP dedup logic. + +**Files:** +- Modify: `spec/specification.md:1029` (step 10) +- Modify: `src/content/docs/specification/install-lifecycle.mdx` (matching step) + +**Step 1: Replace step 10 in spec** + +Replace the current step 10: + +> 10. **Render templates.** The installer processes `.mcp.json` and `.lsp.json` templates, replacing `${config.VARIABLE_NAME}` markers with resolved values. Rendered files are written to the install location. + +With: + +```markdown +10. **Render templates and deduplicate MCP servers.** The installer processes `.mcp.json` and `.lsp.json` templates, replacing `${config.VARIABLE_NAME}` markers with resolved values. For MCP servers, the installer SHOULD check for duplicates before writing: + + a. For each server entry, compute its identity tuple (key_name, origin) as defined in [Server Deduplication](#server-deduplication). + + b. If no matching entry exists in `shared_mcp_servers`: render the template, merge into the host config, and add the server to `shared_mcp_servers` with `declared_by` set to the current package. + + c. If a match exists and the incoming version is higher: re-render using the incoming package's template, update `active_source` and `version`, and append the package to `declared_by`. + + d. If a match exists and the incoming version is equal or lower: skip rendering and append the package to `declared_by` only. + + e. If the key_name matches but the origin differs: warn the user and offer resolution options (keep, replace, or install both under distinct keys). + + f. If the user has disabled dedup for this server (`dedup: false`), skip dedup checks and install the server as a separate entry. + + Rendered `.lsp.json` files are written to the install location without deduplication (LSP server dedup is deferred to a future spec version). +``` + +**Step 2: Update the Mermaid sequence diagram** + +In the install sequence diagram (line ~994), update the "Render templates" step label to "Render templates + dedup MCP". + +**Step 3: Mirror in install-lifecycle.mdx** + +Apply the same changes to `src/content/docs/specification/install-lifecycle.mdx`. + +**Step 4: Verify website builds** + +Run: `npm run build` +Expected: Build succeeds. + +**Step 5: Commit** + +```bash +git add spec/specification.md src/content/docs/specification/install-lifecycle.mdx +git commit -m "docs(spec): revise install step 10 with MCP dedup logic" +``` + +--- + +### Task 5: Revise Uninstall Lifecycle Step 3 + +Update the uninstall sequence to handle shared MCP server reassignment. + +**Files:** +- Modify: `spec/specification.md:1047` (uninstall step 3) +- Modify: `src/content/docs/specification/install-lifecycle.mdx` (uninstall section) + +**Step 1: Replace uninstall step 3 in spec** + +Replace the current step 3: + +> 3. **Remove merged MCP servers.** Remove any MCP server entries that were merged into `.mcp.json` during install (tracked in the lockfile's `merged_mcp_servers` field). + +With: + +```markdown +3. **Remove or reassign MCP servers.** For each MCP server the package declared: + + a. If this package is the only entry in the server's `declared_by` list: remove the server from the host config and from `shared_mcp_servers`. + + b. If other packages remain in `declared_by`: remove this package from the list. If this package was the `active_source`, select the remaining package with the highest version, re-extract its MCP template from the archive cache, re-render with that package's config values, and update `active_source`. If this package was not the active source, no config change is needed. + + c. If the server has `dedup: false`: remove only this package's copy. Other packages' copies are independent and unaffected. +``` + +**Step 2: Mirror in install-lifecycle.mdx** + +Apply the same changes to the uninstall section of `src/content/docs/specification/install-lifecycle.mdx`. + +**Step 3: Verify website builds** + +Run: `npm run build` +Expected: Build succeeds. + +**Step 4: Commit** + +```bash +git add spec/specification.md src/content/docs/specification/install-lifecycle.mdx +git commit -m "docs(spec): revise uninstall step 3 with MCP server reassignment" +``` + +--- + +### Task 6: Add Archive Cache Section + +Formalize the archive cache requirement for re-extract support. + +**Files:** +- Modify: `spec/specification.md` (new section after Lockfile, before Remote Component References) +- Modify: `src/content/docs/specification/lockfile.mdx` (add cache subsection) + +**Step 1: Add Archive Cache section to spec** + +After the Lockfile Format section's Usage subsection (line ~1273), before the Remote Component References section, insert: + +```markdown +## Archive Cache + +Installers MUST maintain a local cache of installed `.ccpkg` archives to support MCP server reassignment on uninstall (see [Uninstall](#uninstall)). + +### Location + +| Scope | Cache Path | +|---|---| +| User | `~/.ccpkg/cache/archives/{name}-{version}.ccpkg` | +| Project | `{project-root}/.ccpkg/cache/archives/{name}-{version}.ccpkg` | + +### Retention + +- Archives for packages referenced by any `declared_by` list in `shared_mcp_servers` MUST be retained. +- Archives for packages not referenced by any `declared_by` list MAY be evicted. +- Installers MAY provide a cache cleanup command to reclaim disk space. +``` + +**Step 2: Mirror in lockfile.mdx** + +Add the same Archive Cache subsection to `src/content/docs/specification/lockfile.mdx` after the Usage section. + +**Step 3: Verify website builds** + +Run: `npm run build` +Expected: Build succeeds. + +**Step 4: Commit** + +```bash +git add spec/specification.md src/content/docs/specification/lockfile.mdx +git commit -m "docs(spec): add archive cache section for MCP dedup re-extract" +``` + +--- + +### Task 7: Add Design Decision 16 to Rationale Doc + +Document the MCP dedup design decision in the design rationale. + +**Files:** +- Modify: `docs/plans/2026-02-14-ccpkg-design.md` (add design decision 16) +- Modify: `src/content/docs/design/rationale.mdx` (add matching decision) + +**Step 1: Add Design Decision 16 to design doc** + +At the end of the Design Decisions section, add: + +```markdown +### 16. MCP server deduplication at install time + +When multiple packages bundle the same MCP server, the installer deduplicates at install time rather than requiring packages to declare shared dependencies or a shared MCP directory. + +**Why install-time dedup?** Packages stay self-contained. Authors do not need to change anything. The dedup is transparent -- the installer is smarter about what it writes to the host config. This preserves Principle #1 (self-contained) and Principle #8 (no inter-package deps). + +**Why not shared directory with refcounting?** Reference counting introduces a new complexity vector. Crashes mid-uninstall corrupt counts. It breaks the "each plugin is self-contained in its directory" model hosts expect. + +**Why not a server_id manifest field?** Requires schema change and author adoption. Existing packages would not benefit. The key_name + origin tuple provides sufficient identity without opt-in. + +**Identity model:** (key_name, origin) tuple. Origin is derived from server mode: command string for Mode 1, bundle path for Mode 2, source URL for Mode 3. Version resolution: highest wins. User override: per-server or global. +``` + +**Step 2: Mirror in rationale.mdx** + +Add the same decision to `src/content/docs/design/rationale.mdx`. + +**Step 3: Verify website builds** + +Run: `npm run build` +Expected: Build succeeds. + +**Step 4: Commit** + +```bash +git add docs/plans/2026-02-14-ccpkg-design.md src/content/docs/design/rationale.mdx +git commit -m "docs: add design decision 16 for MCP server dedup strategy" +``` + +--- + +### Task 8: Run /human-voice:fix and Final Verification + +Clean up any AI-telltale characters and verify everything builds. + +**Files:** +- All modified files from Tasks 1-7 + +**Step 1: Run human-voice fix** + +Run `/human-voice:fix` on `spec/` and `docs/plans/` and `src/content/docs/` to catch any em dashes, smart quotes, or arrows introduced during editing. + +**Step 2: Final build verification** + +Run: `npm run build` +Expected: Build succeeds with all pages generated. + +**Step 3: Commit any fixes** + +```bash +git add -A +git commit -m "style: fix AI-telltale characters in MCP dedup spec text" +``` + +(Skip if no changes.) + +**Step 4: Push and close issue** + +```bash +git push origin main +gh issue close 8 --comment "Resolved. MCP server deduplication added to spec." +``` From 5d18acfa0886128f71198f0a45fbf0729278436c Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:10:43 -0500 Subject: [PATCH 03/11] docs(spec): add design principle #7 for MCP server deduplication --- spec/specification.md | 2 ++ src/content/docs/specification/overview.mdx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/spec/specification.md b/spec/specification.md index 74778f9..c49e0b2 100644 --- a/spec/specification.md +++ b/spec/specification.md @@ -57,6 +57,8 @@ The following principles guide the design of the ccpkg format. Implementors SHOU 6. **No install-time code execution.** Packages MUST NOT execute arbitrary code during installation. There are no postinstall scripts, no build steps, and no setup hooks. The installation process is purely declarative: extract, configure, register. +7. **MCP server deduplication.** When multiple packages declare MCP servers with the same identity (key name and origin), the installer SHOULD deduplicate them. The highest version wins by default. Users MUST be able to override deduplication per-server or globally. + 8. **No inter-package dependencies.** Inter-package dependencies are explicitly out of scope for this specification version. Each package MUST be self-contained and MUST NOT declare dependencies on other ccpkg packages. If a skill requires an MCP server, both MUST be packaged together in a single `.ccpkg` archive. --- diff --git a/src/content/docs/specification/overview.mdx b/src/content/docs/specification/overview.mdx index 727183e..916b95c 100644 --- a/src/content/docs/specification/overview.mdx +++ b/src/content/docs/specification/overview.mdx @@ -56,6 +56,8 @@ The following principles guide the design of the ccpkg format. Implementors SHOU 6. **No install-time code execution.** Packages MUST NOT execute arbitrary code during installation. There are no postinstall scripts, no build steps, and no setup hooks. The installation process is purely declarative: extract, configure, register. +7. **MCP server deduplication.** When multiple packages declare MCP servers with the same identity (key name and origin), the installer SHOULD deduplicate them. The highest version wins by default. Users MUST be able to override deduplication per-server or globally. + 8. **No inter-package dependencies.** Inter-package dependencies are explicitly out of scope for this specification version. Each package MUST be self-contained and MUST NOT declare dependencies on other ccpkg packages. If a skill requires an MCP server, both MUST be packaged together in a single `.ccpkg` archive. --- From 1cad9a58eaefc7642414e779db73acc818ffd813 Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:12:28 -0500 Subject: [PATCH 04/11] docs(spec): add MCP server deduplication identity model and rules --- spec/specification.md | 25 +++++++++++++++++++ .../docs/specification/component-types.mdx | 25 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/spec/specification.md b/spec/specification.md index c49e0b2..e2a4482 100644 --- a/spec/specification.md +++ b/spec/specification.md @@ -799,6 +799,31 @@ Template variables use the syntax `${config.VARIABLE_NAME}`. The variable name M - If an optional config variable is missing and has a default, the default MUST be used. - If an optional config variable is missing and has no default, the template variable MUST be replaced with an empty string. +**Server Deduplication:** + +When installing a package that declares an MCP server already present in the host configuration, the installer SHOULD deduplicate rather than creating a duplicate entry. + +**Server Identity.** An MCP server's identity is a tuple of (key_name, origin): + +- **key_name**: The key in the `mcpServers` object (e.g., `"context7"`). +- **origin**: Derived from the server mode: + - Mode 1 (command+args): `command::{command} {args[0]}` (e.g., `command::npx -y @anthropic/context7-mcp`). + - Mode 2 (embedded mcpb): `bundle::{bundle_path}` normalized to the archive-relative path. + - Mode 3 (referenced mcpb): the `source` URL verbatim. + +Two servers are considered the same when both key_name and origin match. + +**Version Resolution.** Version is extracted from the origin where possible (npm package version, URL path segment, mcpb metadata). + +- Same identity, incoming version higher: replace. Re-render MCP config from the incoming package's template. +- Same identity, incoming version equal or lower: skip rendering. Track in lockfile only. +- Same key_name, different origin: conflict. The installer MUST warn the user. In interactive mode, the installer SHOULD offer to keep the existing server, replace it, or install both under distinct keys. + +**User Override.** Deduplication is the default behavior. Installers MUST provide a mechanism for users to override deduplication: + +- A global flag (e.g., `--no-dedup`) that bypasses all MCP deduplication for the current install operation. +- A per-server override stored in the lockfile's `shared_mcp_servers` entry (`"dedup": false`). When dedup is disabled for a server, each package gets its own independent copy. + ### LSP Servers LSP (Language Server Protocol) server configurations enable packages to provide language intelligence features such as diagnostics, completions, and code actions. diff --git a/src/content/docs/specification/component-types.mdx b/src/content/docs/specification/component-types.mdx index 879628c..46ed19e 100644 --- a/src/content/docs/specification/component-types.mdx +++ b/src/content/docs/specification/component-types.mdx @@ -284,6 +284,31 @@ Template variables use the syntax `${config.VARIABLE_NAME}`. The variable name M - If an optional config variable is missing and has a default, the default MUST be used. - If an optional config variable is missing and has no default, the template variable MUST be replaced with an empty string. +### Server Deduplication + +When installing a package that declares an MCP server already present in the host configuration, the installer SHOULD deduplicate rather than creating a duplicate entry. + +**Server Identity.** An MCP server's identity is a tuple of (key_name, origin): + +- **key_name**: The key in the `mcpServers` object (e.g., `"context7"`). +- **origin**: Derived from the server mode: + - Mode 1 (command+args): `command::{command} {args[0]}` (e.g., `command::npx -y @anthropic/context7-mcp`). + - Mode 2 (embedded mcpb): `bundle::{bundle_path}` normalized to the archive-relative path. + - Mode 3 (referenced mcpb): the `source` URL verbatim. + +Two servers are considered the same when both key_name and origin match. + +**Version Resolution.** Version is extracted from the origin where possible (npm package version, URL path segment, mcpb metadata). + +- Same identity, incoming version higher: replace. Re-render MCP config from the incoming package's template. +- Same identity, incoming version equal or lower: skip rendering. Track in lockfile only. +- Same key_name, different origin: conflict. The installer MUST warn the user. In interactive mode, the installer SHOULD offer to keep the existing server, replace it, or install both under distinct keys. + +**User Override.** Deduplication is the default behavior. Installers MUST provide a mechanism for users to override deduplication: + +- A global flag (e.g., `--no-dedup`) that bypasses all MCP deduplication for the current install operation. +- A per-server override stored in the lockfile's `shared_mcp_servers` entry (`"dedup": false`). When dedup is disabled for a server, each package gets its own independent copy. + ## LSP Servers LSP (Language Server Protocol) server configurations enable packages to provide language intelligence features such as diagnostics, completions, and code actions. From dd51543e5e76983d340abaec1ffe2ba4b940ff7c Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:14:16 -0500 Subject: [PATCH 05/11] docs(spec): add shared_mcp_servers lockfile field for dedup tracking --- spec/specification.md | 23 +++++++++++++++++++++ src/content/docs/specification/lockfile.mdx | 23 +++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/spec/specification.md b/spec/specification.md index e2a4482..ef7bec8 100644 --- a/spec/specification.md +++ b/spec/specification.md @@ -1252,6 +1252,16 @@ The lockfile records the state of all installed packages at a given scope. It en "skills": ["skills/dev-helper"] } } + }, + "shared_mcp_servers": { + "context7": { + "origin": "command::npx -y @anthropic/context7-mcp", + "version": "1.3.0", + "declared_by": ["plugin-a", "plugin-b"], + "active_source": "plugin-b", + "dedup": true, + "installed_at": "2026-02-15T12:00:00Z" + } } } ``` @@ -1292,6 +1302,19 @@ The lockfile records the state of all installed packages at a given scope. It en | `fetched_at` | `string` | ISO 8601 timestamp of last successful fetch | | `cache_ttl` | `number` | Cache duration in seconds from the manifest declaration | +**Shared MCP server fields:** + +The `shared_mcp_servers` top-level field tracks MCP servers that are declared by multiple packages. Keys are MCP server key names. + +| Field | Type | Description | +|---|---|---| +| `origin` | `string` | Identity origin string derived from server mode (see Server Deduplication in [Component Types](#component-types)). | +| `version` | `string \| null` | Resolved winning version. `null` if the server version cannot be determined. | +| `declared_by` | `string[]` | Package names that bundle this server. | +| `active_source` | `string` | Name of the package whose MCP template is currently rendered in the host config. | +| `dedup` | `boolean` | Whether deduplication is active for this server. Defaults to `true`. When `false`, each package installs its own independent copy. | +| `installed_at` | `string` | ISO 8601 timestamp of the last resolution event. | + ### Usage - **Project lockfiles** (`{project-root}/.ccpkg/ccpkg-lock.json`) SHOULD be committed to version control. This allows team members to reproduce the same package environment. diff --git a/src/content/docs/specification/lockfile.mdx b/src/content/docs/specification/lockfile.mdx index 9fc64ac..56ff9f8 100644 --- a/src/content/docs/specification/lockfile.mdx +++ b/src/content/docs/specification/lockfile.mdx @@ -35,6 +35,16 @@ The lockfile records the state of all installed packages at a given scope. It en "mcp": "mcp/.mcp.json" } } + }, + "shared_mcp_servers": { + "context7": { + "origin": "command::npx -y @anthropic/context7-mcp", + "version": "1.3.0", + "declared_by": ["plugin-a", "plugin-b"], + "active_source": "plugin-b", + "dedup": true, + "installed_at": "2026-02-15T12:00:00Z" + } } } ``` @@ -59,6 +69,19 @@ The lockfile records the state of all installed packages at a given scope. It en | `config_hash` | `string` | SHA-256 hash of the resolved config values (excluding secrets). Used to detect config drift. | | `components` | `object` | Mirror of the manifest `components` object for quick reference. | +**Shared MCP server fields:** + +The `shared_mcp_servers` top-level field tracks MCP servers that are declared by multiple packages. Keys are MCP server key names. + +| Field | Type | Description | +|---|---|---| +| `origin` | `string` | Identity origin string derived from server mode (see Server Deduplication in [Component Types](/specification/component-types/)). | +| `version` | `string \| null` | Resolved winning version. `null` if the server version cannot be determined. | +| `declared_by` | `string[]` | Package names that bundle this server. | +| `active_source` | `string` | Name of the package whose MCP template is currently rendered in the host config. | +| `dedup` | `boolean` | Whether deduplication is active for this server. Defaults to `true`. When `false`, each package installs its own independent copy. | +| `installed_at` | `string` | ISO 8601 timestamp of the last resolution event. | + ## Usage - **Project lockfiles** (`{project-root}/.claude/ccpkg-lock.json`) SHOULD be committed to version control. This allows team members to reproduce the same package environment. From 542c3bbfc3eb865e23d0ce3fd8619147bbbf0e0b Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:37:59 -0500 Subject: [PATCH 06/11] docs(spec): revise install step 10 with MCP dedup logic --- spec/specification.md | 18 ++++++++++++++++-- .../docs/specification/install-lifecycle.mdx | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/spec/specification.md b/spec/specification.md index ef7bec8..59ae791 100644 --- a/spec/specification.md +++ b/spec/specification.md @@ -1018,7 +1018,7 @@ sequenceDiagram User->>Installer: Provide config values Installer->>Installer: Resolve install scope Installer->>Installer: Extract archive to install location - Installer->>Installer: Render templates (variable substitution) + Installer->>Installer: Render templates + dedup MCP Installer->>Installer: Store config values in host settings Installer->>Installer: Generate .claude-plugin/plugin.json Installer->>Host: Add to enabledPlugins in settings.json @@ -1053,7 +1053,21 @@ sequenceDiagram If a previous version exists at the install location, the installer MUST remove it before extraction. -10. **Render templates.** The installer processes `.mcp.json` and `.lsp.json` templates, replacing `${config.VARIABLE_NAME}` markers with resolved values. Rendered files are written to the install location. +10. **Render templates and deduplicate MCP servers.** The installer processes `.mcp.json` and `.lsp.json` templates, replacing `${config.VARIABLE_NAME}` markers with resolved values. For MCP servers, the installer SHOULD check for duplicates before writing: + + a. For each server entry, compute its identity tuple (key_name, origin) as defined in Server Deduplication (see [Component Types](#component-types)). + + b. If no matching entry exists in `shared_mcp_servers`: render the template, merge into the host config, and add the server to `shared_mcp_servers` with `declared_by` set to the current package. + + c. If a match exists and the incoming version is higher: re-render using the incoming package's template, update `active_source` and `version`, and append the package to `declared_by`. + + d. If a match exists and the incoming version is equal or lower: skip rendering and append the package to `declared_by` only. + + e. If the key_name matches but the origin differs: warn the user and offer resolution options (keep, replace, or install both under distinct keys). + + f. If the user has disabled dedup for this server (`dedup: false`), skip dedup checks and install the server as a separate entry. + + Rendered `.lsp.json` files are written to the install location without deduplication (LSP server dedup is deferred to a future spec version). 11. **Store config.** Config values are persisted in the host's settings file under `packages.{name}`. diff --git a/src/content/docs/specification/install-lifecycle.mdx b/src/content/docs/specification/install-lifecycle.mdx index 0d12f55..d9f9430 100644 --- a/src/content/docs/specification/install-lifecycle.mdx +++ b/src/content/docs/specification/install-lifecycle.mdx @@ -24,7 +24,7 @@ sequenceDiagram User->>Installer: Provide config values Installer->>Installer: Resolve install scope Installer->>Installer: Extract archive to install location - Installer->>Installer: Render templates (variable substitution) + Installer->>Installer: Render templates + dedup MCP Installer->>Installer: Store config values in host settings Installer->>Installer: Update lockfile Installer->>Host: Register components @@ -58,7 +58,21 @@ sequenceDiagram If a previous version exists at the install location, the installer MUST remove it before extraction. -10. **Render templates.** The installer processes `.mcp.json` and `.lsp.json` templates, replacing `${config.VARIABLE_NAME}` markers with resolved values. Rendered files are written to the install location. +10. **Render templates and deduplicate MCP servers.** The installer processes `.mcp.json` and `.lsp.json` templates, replacing `${config.VARIABLE_NAME}` markers with resolved values. For MCP servers, the installer SHOULD check for duplicates before writing: + + a. For each server entry, compute its identity tuple (key_name, origin) as defined in Server Deduplication (see [Component Types](/specification/component-types/)). + + b. If no matching entry exists in `shared_mcp_servers`: render the template, merge into the host config, and add the server to `shared_mcp_servers` with `declared_by` set to the current package. + + c. If a match exists and the incoming version is higher: re-render using the incoming package's template, update `active_source` and `version`, and append the package to `declared_by`. + + d. If a match exists and the incoming version is equal or lower: skip rendering and append the package to `declared_by` only. + + e. If the key_name matches but the origin differs: warn the user and offer resolution options (keep, replace, or install both under distinct keys). + + f. If the user has disabled dedup for this server (`dedup: false`), skip dedup checks and install the server as a separate entry. + + Rendered `.lsp.json` files are written to the install location without deduplication (LSP server dedup is deferred to a future spec version). 11. **Store config.** Config values are persisted in the host's settings file under `packages.{name}`. From 951780cc624f7ab120e51ff14e8cde86068a062f Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:38:54 -0500 Subject: [PATCH 07/11] docs(spec): revise uninstall step 3 with MCP server reassignment --- spec/specification.md | 8 +++++++- .../docs/specification/install-lifecycle.mdx | 14 +++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/spec/specification.md b/spec/specification.md index 59ae791..59d83b0 100644 --- a/spec/specification.md +++ b/spec/specification.md @@ -1085,7 +1085,13 @@ Uninstalling a package reverses the install process: 1. **Remove the package directory** from the install location (`~/.ccpkg/plugins/{name}/` or `{project-root}/.ccpkg/plugins/{name}/`). 2. **Remove from enabledPlugins.** Remove the `{name}@ccpkg` entry from the host's `enabledPlugins` in `settings.json`. -3. **Remove merged MCP servers.** Remove any MCP server entries that were merged into `.mcp.json` during install (tracked in the lockfile's `merged_mcp_servers` field). +3. **Remove or reassign MCP servers.** For each MCP server the package declared: + + a. If this package is the only entry in the server's `declared_by` list: remove the server from the host config and from `shared_mcp_servers`. + + b. If other packages remain in `declared_by`: remove this package from the list. If this package was the `active_source`, select the remaining package with the highest version, re-extract its MCP template from the archive cache, re-render with that package's config values, and update `active_source`. If this package was not the active source, no config change is needed. + + c. If the server has `dedup: false`: remove only this package's copy. Other packages' copies are independent and unaffected. 4. **Remove lockfile entry.** Remove the package entry from `ccpkg-lock.json`. 5. **Remove config values.** Remove config values from host settings. Secrets SHOULD require explicit user confirmation before removal. 6. **Notify user.** Inform the user that a session restart is required to fully deactivate the package's components. diff --git a/src/content/docs/specification/install-lifecycle.mdx b/src/content/docs/specification/install-lifecycle.mdx index d9f9430..b0d35d2 100644 --- a/src/content/docs/specification/install-lifecycle.mdx +++ b/src/content/docs/specification/install-lifecycle.mdx @@ -85,9 +85,17 @@ sequenceDiagram Uninstalling a package reverses the install process: 1. Remove the package directory from the install location. -2. Remove the package entry from the lockfile. -3. Remove config values from host settings (except secrets, which SHOULD require explicit confirmation). -4. Deregister components from the host. +2. Remove or reassign MCP servers. For each MCP server the package declared: + + a. If this package is the only entry in the server's `declared_by` list: remove the server from the host config and from `shared_mcp_servers`. + + b. If other packages remain in `declared_by`: remove this package from the list. If this package was the `active_source`, select the remaining package with the highest version, re-extract its MCP template from the archive cache, re-render with that package's config values, and update `active_source`. If this package was not the active source, no config change is needed. + + c. If the server has `dedup: false`: remove only this package's copy. Other packages' copies are independent and unaffected. + +3. Remove the package entry from the lockfile. +4. Remove config values from host settings (except secrets, which SHOULD require explicit confirmation). +5. Deregister components from the host. ## Update From 5bfe46f61210136fcf13d5915d49f188b988816a Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:39:40 -0500 Subject: [PATCH 08/11] docs(spec): add archive cache section for MCP dedup re-extract --- spec/specification.md | 17 +++++++++++++++++ src/content/docs/specification/lockfile.mdx | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/spec/specification.md b/spec/specification.md index 59d83b0..d3d6b01 100644 --- a/spec/specification.md +++ b/spec/specification.md @@ -1343,6 +1343,23 @@ The `shared_mcp_servers` top-level field tracks MCP servers that are declared by --- +## Archive Cache + +Installers MUST maintain a local cache of installed `.ccpkg` archives to support MCP server reassignment on uninstall (see [Uninstall](#uninstall)). + +### Location + +| Scope | Cache Path | +|---|---| +| User | `~/.ccpkg/cache/archives/{name}-{version}.ccpkg` | +| Project | `{project-root}/.ccpkg/cache/archives/{name}-{version}.ccpkg` | + +### Retention + +- Archives for packages referenced by any `declared_by` list in `shared_mcp_servers` MUST be retained. +- Archives for packages not referenced by any `declared_by` list MAY be evicted. +- Installers MAY provide a cache cleanup command to reclaim disk space. + --- ## Remote Component References diff --git a/src/content/docs/specification/lockfile.mdx b/src/content/docs/specification/lockfile.mdx index 56ff9f8..153c97d 100644 --- a/src/content/docs/specification/lockfile.mdx +++ b/src/content/docs/specification/lockfile.mdx @@ -87,3 +87,20 @@ The `shared_mcp_servers` top-level field tracks MCP servers that are declared by - **Project lockfiles** (`{project-root}/.claude/ccpkg-lock.json`) SHOULD be committed to version control. This allows team members to reproduce the same package environment. - **User lockfiles** (`~/.claude/ccpkg-lock.json`) are personal and SHOULD NOT be shared. - An installer MAY provide a `ccpkg restore` command that reads the lockfile and installs all listed packages at their recorded versions. + +## Archive Cache + +Installers MUST maintain a local cache of installed `.ccpkg` archives to support MCP server reassignment on uninstall (see [Install Lifecycle](/specification/install-lifecycle/)). + +### Location + +| Scope | Cache Path | +|---|---| +| User | `~/.ccpkg/cache/archives/{name}-{version}.ccpkg` | +| Project | `{project-root}/.ccpkg/cache/archives/{name}-{version}.ccpkg` | + +### Retention + +- Archives for packages referenced by any `declared_by` list in `shared_mcp_servers` MUST be retained. +- Archives for packages not referenced by any `declared_by` list MAY be evicted. +- Installers MAY provide a cache cleanup command to reclaim disk space. From 9eb93b697a466f5a609bedd249642a5a83e1e626 Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:44:41 -0500 Subject: [PATCH 09/11] docs: add design decision 16 for MCP server dedup strategy --- docs/plans/2026-02-14-ccpkg-design.md | 12 ++++++++++++ src/content/docs/design/rationale.mdx | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/docs/plans/2026-02-14-ccpkg-design.md b/docs/plans/2026-02-14-ccpkg-design.md index b7ae93f..4d4f06b 100644 --- a/docs/plans/2026-02-14-ccpkg-design.md +++ b/docs/plans/2026-02-14-ccpkg-design.md @@ -374,6 +374,18 @@ The ccpkg format targets multiple AI coding assistant hosts, but each host has f 2. **Template language with conditionals** -- Use a templating syntax (e.g., Handlebars, Jinja) with `{{#if host == "claude"}}` blocks. Powerful but introduces a template engine dependency, makes the raw files hard to read, and is overkill for what is typically "shared base + small per-host additions." Rejected for complexity. 3. **mappings.json only (previous design)** -- Map a single canonical file to host-specific filenames without any content variation. Already proven insufficient — the filename mapping exists via `targets.*.instructions_file`, but the content is identical everywhere. Superseded by the assembly model which adds content variation on top of filename mapping. +### 16. MCP server deduplication at install time + +When multiple packages bundle the same MCP server, the installer deduplicates at install time rather than requiring packages to declare shared dependencies or a shared MCP directory. + +**Why install-time dedup?** Packages stay self-contained. Authors do not need to change anything. The dedup is transparent -- the installer is smarter about what it writes to the host config. This preserves Principle #1 (self-contained) and Principle #8 (no inter-package deps). + +**Why not shared directory with refcounting?** Reference counting introduces a new complexity vector. Crashes mid-uninstall corrupt counts. It breaks the "each plugin is self-contained in its directory" model hosts expect. + +**Why not a server_id manifest field?** Requires schema change and author adoption. Existing packages would not benefit. The key_name + origin tuple provides sufficient identity without opt-in. + +**Identity model:** (key_name, origin) tuple. Origin is derived from server mode: command string for Mode 1, bundle path for Mode 2, source URL for Mode 3. Version resolution: highest wins. User override: per-server or global. + --- ## Relationship to Existing Specifications diff --git a/src/content/docs/design/rationale.mdx b/src/content/docs/design/rationale.mdx index 491de7c..8307ede 100644 --- a/src/content/docs/design/rationale.mdx +++ b/src/content/docs/design/rationale.mdx @@ -249,6 +249,18 @@ Registries solve discovery. But a mandatory central registry creates a single po **Discovery via search**: `/ccpkg:search` queries configured registries, merges results, and presents them with trust signals. This is the experience gap between "browse GitHub repos" and "find the right package." +### 9. MCP server deduplication at install time + +When multiple packages bundle the same MCP server, the installer deduplicates at install time rather than requiring packages to declare shared dependencies or a shared MCP directory. + +**Why install-time dedup?** Packages stay self-contained. Authors do not need to change anything. The dedup is transparent -- the installer is smarter about what it writes to the host config. This preserves Principle #1 (self-contained) and Principle #8 (no inter-package deps). + +**Why not shared directory with refcounting?** Reference counting introduces a new complexity vector. Crashes mid-uninstall corrupt counts. It breaks the "each plugin is self-contained in its directory" model hosts expect. + +**Why not a server_id manifest field?** Requires schema change and author adoption. Existing packages would not benefit. The key_name + origin tuple provides sufficient identity without opt-in. + +**Identity model:** (key_name, origin) tuple. Origin is derived from server mode: command string for Mode 1, bundle path for Mode 2, source URL for Mode 3. Version resolution: highest wins. User override: per-server or global. + --- ## Relationship to Existing Specifications From 2e4d6c4ea1d9ee69ba25866dcdb3b4d08dce0935 Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 18:50:12 -0500 Subject: [PATCH 10/11] style: fix AI-telltale characters in plan docs --- docs/plans/2026-02-14-ccpkg-design.md | 40 +++---- .../2026-02-14-spec-grounding-revision.md | 108 +++++++++--------- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/docs/plans/2026-02-14-ccpkg-design.md b/docs/plans/2026-02-14-ccpkg-design.md index 4d4f06b..aab4e2a 100644 --- a/docs/plans/2026-02-14-ccpkg-design.md +++ b/docs/plans/2026-02-14-ccpkg-design.md @@ -6,11 +6,11 @@ nav_order: 3 # ccpkg Design Document -| Field | Value | +| Field | Value | |---------|--------------| -| Date | 2026-02-14 | -| Status | Draft | -| Authors | Allen R. | +| Date | 2026-02-14 | +| Status | Draft | +| Authors | Allen R. | --- @@ -161,11 +161,11 @@ Plugins frequently need configuration: API keys, file paths, feature flags, serv ```json { - "config": { - "API_KEY": { "type": "secret", "required": true, "description": "Service API key" }, - "MAX_RESULTS": { "type": "number", "default": 10 }, - "OUTPUT_FORMAT": { "type": "enum", "values": ["json", "text"], "default": "json" } - } + "config": { + "API_KEY": { "type": "secret", "required": true, "description": "Service API key" }, + "MAX_RESULTS": { "type": "number", "default": 10 }, + "OUTPUT_FORMAT": { "type": "enum", "values": ["json", "text"], "default": "json" } + } } ``` @@ -258,14 +258,14 @@ ccpkg packages install as Claude Code plugins. This is the fundamental integrati ```json { - "extraKnownMarketplaces": { - "ccpkg": { - "source": { - "source": "directory", - "path": "~/.ccpkg/plugins" - } - } - } + "extraKnownMarketplaces": { + "ccpkg": { + "source": { + "source": "directory", + "path": "~/.ccpkg/plugins" + } + } + } } ``` @@ -362,7 +362,7 @@ The ccpkg format targets multiple AI coding assistant hosts, but each host has f --- -### 15. Instructions Assembly — Base + Per-Host Overlays +### 15. Instructions Assembly. Base + Per-Host Overlays **Decision**: The `components.instructions` field supports both a simple string form (single file) and a structured form declaring a base file with optional per-host overlay files. Overlays declare their assembly position (`append`, `prepend`, or `insert` at a named marker) via YAML frontmatter. The installer assembles the final instructions output per host at install time. @@ -372,7 +372,7 @@ The ccpkg format targets multiple AI coding assistant hosts, but each host has f 1. **Separate instruction files per host** -- Each host gets its own complete file (`claude-instructions.md`, `copilot-instructions.md`). Simple to implement but leads to content duplication. When shared content changes, authors must update N files. Rejected because it undermines the DRY principle and scales poorly with host count. 2. **Template language with conditionals** -- Use a templating syntax (e.g., Handlebars, Jinja) with `{{#if host == "claude"}}` blocks. Powerful but introduces a template engine dependency, makes the raw files hard to read, and is overkill for what is typically "shared base + small per-host additions." Rejected for complexity. -3. **mappings.json only (previous design)** -- Map a single canonical file to host-specific filenames without any content variation. Already proven insufficient — the filename mapping exists via `targets.*.instructions_file`, but the content is identical everywhere. Superseded by the assembly model which adds content variation on top of filename mapping. +3. **mappings.json only (previous design)** -- Map a single canonical file to host-specific filenames without any content variation. Already proven insufficient. the filename mapping exists via `targets.*.instructions_file`, but the content is identical everywhere. Superseded by the assembly model which adds content variation on top of filename mapping. ### 16. MCP server deduplication at install time @@ -441,7 +441,7 @@ Dev mode uses a symmetric pair of operations that mirror the install/uninstall l 1. Validate the directory contains a valid `manifest.json` 2. Prompt for required config values (same as install) 3. Generate `.claude-plugin/plugin.json` inside the source directory (from manifest metadata) -4. Create symlink: `~/.ccpkg/plugins/{name}` → source directory +4. Create symlink: `~/.ccpkg/plugins/{name}` -> source directory 5. Add `{name}@ccpkg` to `enabledPlugins` in `settings.json` 6. Render MCP/LSP templates with config substitution 7. Write lockfile entry with `"source": "link:/absolute/path"`, `"linked": true`, and `"generated_plugin_json": true` (if plugin.json was created by ccpkg, not pre-existing) diff --git a/docs/plans/2026-02-14-spec-grounding-revision.md b/docs/plans/2026-02-14-spec-grounding-revision.md index 4b6e740..f3389cf 100644 --- a/docs/plans/2026-02-14-spec-grounding-revision.md +++ b/docs/plans/2026-02-14-spec-grounding-revision.md @@ -1,4 +1,4 @@ -# ccpkg Spec Grounding Revision — Implementation Plan +# ccpkg Spec Grounding Revision. Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. @@ -21,35 +21,35 @@ Claude Code's plugin system provides automatic namespacing via `.claude-plugin/p - Plugin with `{"name": "my-package"}` + skill in `skills/my-skill/SKILL.md` = `/my-package:my-skill` - Namespace prefix: driven by `plugin.json` `name` field (NOT directory name, NOT SKILL.md frontmatter) - Component name within namespace: driven by skill **directory name** (NOT frontmatter `name`) -- User-level skills (`~/.claude/skills/`) CANNOT be namespaced — subdirectories flatten -- User-level commands (`~/.claude/commands/`) CANNOT be namespaced — subdirectories flatten +- User-level skills (`~/.claude/skills/`) CANNOT be namespaced. subdirectories flatten +- User-level commands (`~/.claude/commands/`) CANNOT be namespaced. subdirectories flatten - There is NO programmatic API to register namespaced components outside the plugin system **Implication:** ccpkg packages MUST be installed as Claude Code plugins to get namespacing. ### Finding 2: extraKnownMarketplaces Is the Official Registration Entry Point -Claude Code supports third-party plugin sources via `extraKnownMarketplaces` in `settings.json`. This is the documented, supported API — NOT `known_marketplaces.json` (which is internal). +Claude Code supports third-party plugin sources via `extraKnownMarketplaces` in `settings.json`. This is the documented, supported API. NOT `known_marketplaces.json` (which is internal). Seven source types are supported: -1. `github` — GitHub repositories -2. `git` — any Git URL -3. `url` — hosted marketplace.json -4. `npm` — npm packages -5. `file` — local marketplace.json file -6. `directory` — local directory of plugins -7. `hostPattern` — regex host matching +1. `github`. GitHub repositories +2. `git`. any Git URL +3. `url`. hosted marketplace.json +4. `npm`. npm packages +5. `file`. local marketplace.json file +6. `directory`. local directory of plugins +7. `hostPattern`. regex host matching **Implication:** ccpkg registers as a marketplace using the `directory` source type, pointing at `~/.ccpkg/plugins/`. Claude Code auto-discovers plugins in that directory. ### Finding 3: Six Plugin Loading Mechanisms Exist -1. **Marketplace installation** — via `/plugin` UI or `claude plugin install` -2. **`--plugin-dir` CLI flag** — session-only, no persistence, for quick testing -3. **CLI commands** — `claude plugin install/uninstall/enable/disable` -4. **Interactive `/plugin` UI** — 4-tab manager (Discover, Installed, Marketplaces, Errors) -5. **Project-level `.claude/settings.json`** — team-shared, committed to git -6. **Managed settings** — enterprise/org level, highest precedence, read-only +1. **Marketplace installation**. via `/plugin` UI or `claude plugin install` +2. **`--plugin-dir` CLI flag**. session-only, no persistence, for quick testing +3. **CLI commands**. `claude plugin install/uninstall/enable/disable` +4. **Interactive `/plugin` UI**. 4-tab manager (Discover, Installed, Marketplaces, Errors) +5. **Project-level `.claude/settings.json`**. team-shared, committed to git +6. **Managed settings**. enterprise/org level, highest precedence, read-only Scope precedence: Managed > Local > Project > User @@ -74,9 +74,9 @@ Confirmed: no hot-reload mid-session. Changes to plugin registration take effect - Pack/verify commands via ZIP + manifest validation **Aspirational (requires Claude Code application changes):** -- Hot-reload after install (no session restart) — no API to notify Claude Code of new plugins mid-session -- Host-aware lockfile loading — Claude Code does not read `ccpkg-lock.json` at startup -- Runtime component state machine (Idle → Active → Idle) — no host-managed state tracking +- Hot-reload after install (no session restart). no API to notify Claude Code of new plugins mid-session +- Host-aware lockfile loading. Claude Code does not read `ccpkg-lock.json` at startup +- Runtime component state machine (Idle -> Active -> Idle). no host-managed state tracking ### Finding 6: Install Architecture @@ -105,13 +105,13 @@ Confirmed: no hot-reload mid-session. Changes to plugin registration take effect 6. Remove lockfile entry 7. Inform user: session restart to deactivate -### Finding 7: Dev Mode — Symmetric Link/Unlink +### Finding 7: Dev Mode. Symmetric Link/Unlink **`/ccpkg:link ~/Projects/my-plugin`:** 1. Validate directory has valid `manifest.json` 2. Prompt for required config values 3. Generate `.claude-plugin/plugin.json` inside source directory (from manifest) -4. Create symlink `~/.ccpkg/plugins/{name}` → source directory +4. Create symlink `~/.ccpkg/plugins/{name}` -> source directory 5. Add to `enabledPlugins`, render templates, write lockfile with `"source": "link:/absolute/path"` 6. Record `generated_plugin_json: true` in lockfile if plugin.json was created (not pre-existing) @@ -128,24 +128,24 @@ Confirmed: no hot-reload mid-session. Changes to plugin registration take effect The lockfile serves dual purpose: reproducibility record AND uninstall manifest. Expanded fields per package entry: -- `installed_files` — list of all files written during install -- `merged_mcp_servers` — MCP server names merged into `.mcp.json` -- `merged_hooks` — hook entries merged into settings -- `config_keys` — config variable names stored in settings -- `generated_plugin_json` — boolean, whether `.claude-plugin/plugin.json` was generated -- `linked` — boolean, whether this is a dev-linked package -- `source` — URL, file path, or `link:/path` for dev mode +- `installed_files`. list of all files written during install +- `merged_mcp_servers`. MCP server names merged into `.mcp.json` +- `merged_hooks`. hook entries merged into settings +- `config_keys`. config variable names stored in settings +- `generated_plugin_json`. boolean, whether `.claude-plugin/plugin.json` was generated +- `linked`. boolean, whether this is a dev-linked package +- `source`. URL, file path, or `link:/path` for dev mode ### Finding 9: Two Lockfiles, Two Audiences -- `installed_plugins.json` — Claude Code's internal plugin registry (for host discovery) -- `ccpkg-lock.json` — ccpkg's lifecycle manifest (source URLs, checksums, config hashes, provenance) +- `installed_plugins.json`. Claude Code's internal plugin registry (for host discovery) +- `ccpkg-lock.json`. ccpkg's lifecycle manifest (source URLs, checksums, config hashes, provenance) ccpkg writes to BOTH during install. They serve different systems. --- -## Task 1: Revise Specification — Install Lifecycle Section +## Task 1: Revise Specification. Install Lifecycle Section **Files:** - Modify: `spec/specification.md` (lines ~730-830, Install Lifecycle section) @@ -154,7 +154,7 @@ ccpkg writes to BOTH during install. They serve different systems. Replace the current Mermaid sequence diagram. The new diagram must: - Remove the `Host->>User: Components available (no restart required)` step -- Add explicit `Installer->>Installer: Generate .claude-plugin/plugin.json` +- Add explicit `Installer->>Installer: Generate.claude-plugin/plugin.json` - Add explicit `Installer->>Host: Add to enabledPlugins in settings.json` - End with `Installer->>User: Installation complete. Restart session to activate.` @@ -191,7 +191,7 @@ Replace the current 5-step process with the symmetric link/unlink design: --- -## Task 2: Revise Specification — Add Host Integration Section +## Task 2: Revise Specification. Add Host Integration Section **Files:** - Modify: `spec/specification.md` (new section after Install Lifecycle, before Lockfile Format) @@ -219,7 +219,7 @@ Document: Document: - User scope: `extraKnownMarketplaces` in `~/.claude/settings.json` - Project scope: `extraKnownMarketplaces` in `{project}/.claude/settings.json` -- Project settings are committed to git — team members get prompted to install +- Project settings are committed to git. team members get prompted to install - Managed scope: enterprise admins can allowlist ccpkg via `strictKnownMarketplaces` - Scope precedence: Managed > Local > Project > User @@ -227,7 +227,7 @@ Document: --- -## Task 3: Revise Specification — Rewrite Lazy Loading Section +## Task 3: Revise Specification. Rewrite Lazy Loading Section **Files:** - Modify: `spec/specification.md` (lines ~896-923, Lazy Loading section) @@ -238,7 +238,7 @@ Replace the current 5-step host-reads-lockfile description with: - State that Claude Code already implements lazy loading for skills: frontmatter (name + description) is loaded at startup, full SKILL.md body is loaded on invocation via the Skill tool - ccpkg leverages this existing behavior by placing well-formed SKILL.md files in standard plugin directories - No custom host-level lockfile reader is required -- The host discovers ccpkg plugins via `extraKnownMarketplaces` → directory source → standard plugin component discovery +- The host discovers ccpkg plugins via `extraKnownMarketplaces` -> directory source -> standard plugin component discovery **Step 2: Keep "On-Demand Loading" table** @@ -256,7 +256,7 @@ Add a note: "These behaviors are provided by the host application's existing plu --- -## Task 4: Revise Specification — Expand Lockfile Schema +## Task 4: Revise Specification. Expand Lockfile Schema **Files:** - Modify: `spec/specification.md` (lines ~831-895, Lockfile Format section) @@ -285,7 +285,7 @@ Show a complete example with all new fields, including one installed package and --- -## Task 5: Revise Specification — Add Aspirational Appendix +## Task 5: Revise Specification. Add Aspirational Appendix **Files:** - Modify: `spec/specification.md` (new Appendix D after Appendix C) @@ -294,18 +294,18 @@ Show a complete example with all new fields, including one installed package and Move all aspirational content here with clear framing: -Section D.1 — Hot-Reload After Install: +Section D.1. Hot-Reload After Install: - Description: Components become available immediately after install without session restart - Requires: A host API or file-watch mechanism to detect new plugins mid-session - Current behavior: Session restart required -Section D.2 — Host-Aware Lockfile Loading: +Section D.2. Host-Aware Lockfile Loading: - Description: Host reads `ccpkg-lock.json` at startup for optimized package discovery - Requires: Host application to understand ccpkg lockfile format - Current behavior: Host discovers packages via `extraKnownMarketplaces` and standard plugin directories -Section D.3 — Runtime Component State Machine: -- Description: Components track Idle → Active → Idle lifecycle states +Section D.3. Runtime Component State Machine: +- Description: Components track Idle -> Active -> Idle lifecycle states - Requires: Host-managed activation tracking per component - Current behavior: Components are either installed (files exist in plugin directory) or not @@ -313,7 +313,7 @@ Section D.3 — Runtime Component State Machine: --- -## Task 6: Revise Specification — Update Archive Directory Structure +## Task 6: Revise Specification. Update Archive Directory Structure **Files:** - Modify: `spec/specification.md` (lines ~136-169, Directory Structure) @@ -322,14 +322,14 @@ Section D.3 — Runtime Component State Machine: The archive structure itself does not change. But add a note after the diagram explaining: - At install time, the installer generates `.claude-plugin/plugin.json` from `manifest.json` metadata -- This generated file is NOT part of the archive — it is a host-specific artifact created during installation -- The mapping: manifest `name` → plugin.json `name`, manifest `version` → plugin.json `version`, manifest `description` → plugin.json `description`, manifest `author` → plugin.json `author` +- This generated file is NOT part of the archive. it is a host-specific artifact created during installation +- The mapping: manifest `name` -> plugin.json `name`, manifest `version` -> plugin.json `version`, manifest `description` -> plugin.json `description`, manifest `author` -> plugin.json `author` **Step 2: Commit** --- -## Task 7: Revise Design Document — Add Plugin System Integration +## Task 7: Revise Design Document. Add Plugin System Integration **Files:** - Modify: `docs/plans/2026-02-14-ccpkg-design.md` @@ -378,10 +378,10 @@ Replace with the symmetric link/unlink design: **Step 1: Review schema for any required changes** -The manifest schema itself should NOT change significantly — the manifest is the package author's contract. The `.claude-plugin/plugin.json` generation is an installer concern, not a manifest concern. +The manifest schema itself should NOT change significantly. the manifest is the package author's contract. The `.claude-plugin/plugin.json` generation is an installer concern, not a manifest concern. However, verify: -- The `scope` enum still makes sense (it does — `user`, `project`, `any`) +- The `scope` enum still makes sense (it does. `user`, `project`, `any`) - The `targets` object can accommodate plugin.json generation hints if needed - No fields reference the old `~/.claude/packages/` path @@ -424,7 +424,7 @@ Capture the following to mnemonic: - `_semantic/decisions`: ccpkg installs as Claude Code plugins via extraKnownMarketplaces directory source - `_semantic/decisions`: Namespacing handled by plugin system, not file editing - `_semantic/knowledge`: installed_plugins.json is read-only at startup, no hot-reload -- `_procedural/patterns`: Symmetric link/unlink manages .claude-plugin/plugin.json lifecycle +- `_procedural/patterns`: Symmetric link/unlink manages.claude-plugin/plugin.json lifecycle - `_semantic/knowledge`: Six plugin loading mechanisms in Claude Code (marketplace, --plugin-dir, CLI, /plugin UI, project settings, managed settings) --- @@ -461,7 +461,7 @@ Each task ends with a commit. Commit messages follow: ### What This Plan Does NOT Cover -- Implementation of ccpkg CLI/skills (pack, install, verify, etc.) — that is a separate plan -- Implementation of the registry protocol — that is a separate plan -- Testing — no tests exist yet for the spec itself -- GitHub Pages deployment — the deploy.yml workflow already exists +- Implementation of ccpkg CLI/skills (pack, install, verify, etc.). that is a separate plan +- Implementation of the registry protocol. that is a separate plan +- Testing. no tests exist yet for the spec itself +- GitHub Pages deployment. the deploy.yml workflow already exists From bc298d81ecf275205b54a67af76ea9fdf37b7ed0 Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Sun, 15 Feb 2026 19:27:20 -0500 Subject: [PATCH 11/11] fix(spec): address Copilot review feedback on MCP dedup - Fix Mode 1 origin to use full args array instead of args[0] - Add deterministic null version comparison rules - Define non-interactive conflict resolution (fail with error) - Specify package-scoped keying for dedup:false entries - Clarify uninstall reassignment for linked packages - Align cache paths in lockfile.mdx with Astro docs convention - Update merged_mcp_servers field description in spec --- spec/specification.md | 23 ++++++++++++------- .../docs/specification/component-types.mdx | 5 ++-- .../docs/specification/install-lifecycle.mdx | 16 +++++++++---- src/content/docs/specification/lockfile.mdx | 4 ++-- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/spec/specification.md b/spec/specification.md index d3d6b01..a289eec 100644 --- a/spec/specification.md +++ b/spec/specification.md @@ -807,7 +807,7 @@ When installing a package that declares an MCP server already present in the hos - **key_name**: The key in the `mcpServers` object (e.g., `"context7"`). - **origin**: Derived from the server mode: - - Mode 1 (command+args): `command::{command} {args[0]}` (e.g., `command::npx -y @anthropic/context7-mcp`). + - Mode 1 (command+args): `command::{command} {args_joined}`, where `{args_joined}` is all elements of the `args` array joined with a single space. If `args` is empty or omitted, the origin is `command::{command}` (no trailing space). For example, `command: "npx"`, `args: ["-y", "@anthropic/context7-mcp"]` yields `command::npx -y @anthropic/context7-mcp`. - Mode 2 (embedded mcpb): `bundle::{bundle_path}` normalized to the archive-relative path. - Mode 3 (referenced mcpb): the `source` URL verbatim. @@ -817,7 +817,8 @@ Two servers are considered the same when both key_name and origin match. - Same identity, incoming version higher: replace. Re-render MCP config from the incoming package's template. - Same identity, incoming version equal or lower: skip rendering. Track in lockfile only. -- Same key_name, different origin: conflict. The installer MUST warn the user. In interactive mode, the installer SHOULD offer to keep the existing server, replace it, or install both under distinct keys. +- When comparing versions, treat `null`/unknown as lower than any concrete semver version. If both are `null`/unknown, keep the existing server (treat as equal). +- Same key_name, different origin: conflict. In interactive mode, the installer MUST warn the user and offer to keep the existing server, replace it, or install both under distinct keys. In non-interactive mode, the installer MUST fail with a descriptive error (implementations MAY support a preconfigured conflict policy). **User Override.** Deduplication is the default behavior. Installers MUST provide a mechanism for users to override deduplication: @@ -1059,13 +1060,19 @@ sequenceDiagram b. If no matching entry exists in `shared_mcp_servers`: render the template, merge into the host config, and add the server to `shared_mcp_servers` with `declared_by` set to the current package. - c. If a match exists and the incoming version is higher: re-render using the incoming package's template, update `active_source` and `version`, and append the package to `declared_by`. + c. If a match exists, compare versions. Treat `null`/unknown versions as lower than any concrete semver version. If both are `null`/unknown, treat them as equal. - d. If a match exists and the incoming version is equal or lower: skip rendering and append the package to `declared_by` only. + - If the incoming version is higher: re-render using the incoming package's template, update `active_source` and `version`, and append the package to `declared_by`. - e. If the key_name matches but the origin differs: warn the user and offer resolution options (keep, replace, or install both under distinct keys). + - If the incoming version is equal or lower (including both `null`): skip rendering and append the package to `declared_by` only. - f. If the user has disabled dedup for this server (`dedup: false`), skip dedup checks and install the server as a separate entry. + d. If the key_name matches but the origin differs (conflict): + + - **Interactive mode**: warn the user and offer resolution options: keep the existing server, replace it, or install both under distinct keys. + + - **Non-interactive mode**: fail the install with a non-zero exit status and a descriptive error. Implementations MAY allow a preconfigured conflict policy (e.g., via CLI flags) to resolve without prompting. + + e. If the user has disabled dedup for this server (`dedup: false`), skip dedup checks and install the server under a package-scoped key (e.g., `{key_name}#{package_name}`). The installer MUST record the concrete key in the lockfile so uninstall can identify the correct entry. Rendered `.lsp.json` files are written to the install location without deduplication (LSP server dedup is deferred to a future spec version). @@ -1089,7 +1096,7 @@ Uninstalling a package reverses the install process: a. If this package is the only entry in the server's `declared_by` list: remove the server from the host config and from `shared_mcp_servers`. - b. If other packages remain in `declared_by`: remove this package from the list. If this package was the `active_source`, select the remaining package with the highest version, re-extract its MCP template from the archive cache, re-render with that package's config values, and update `active_source`. If this package was not the active source, no config change is needed. + b. If other packages remain in `declared_by`: remove this package from the list. If this package was the `active_source`, select the remaining package with the highest version, re-materialize its MCP template, re-render with that package's config values, and update `active_source`. For archive-backed packages, re-extract from the archive cache. For linked packages (`source: link:...`), read the template directly from the linked directory. If this package was not the active source, no config change is needed. c. If the server has `dedup: false`: remove only this package's copy. Other packages' copies are independent and unaffected. 4. **Remove lockfile entry.** Remove the package entry from `ccpkg-lock.json`. @@ -1308,7 +1315,7 @@ The lockfile records the state of all installed packages at a given scope. It en | `generated_plugin_json` | `boolean` | Whether `.claude-plugin/plugin.json` was generated by ccpkg during install/link. Controls cleanup on uninstall/unlink — if `true`, the generated file is removed; if `false`, it is left in place. | | `enabled_plugins_key` | `string` | The key written to the host's `enabledPlugins` (e.g., `"api-testing@ccpkg"`). Used for clean deregistration on uninstall. | | `installed_files` | `string[]` | List of all files written during install, relative to the package directory. Enables deterministic uninstall. Empty for linked packages. | -| `merged_mcp_servers` | `string[]` | MCP server names merged into the host's `.mcp.json` during install. Used for clean removal on uninstall. | +| `merged_mcp_servers` | `string[]` | MCP server names merged into the host's `.mcp.json` during install. Superseded by `shared_mcp_servers` for dedup-aware uninstall tracking. Retained for packages that do not participate in dedup (single-server packages with no shared MCP servers). | | `config_keys` | `string[]` | Config variable names stored in the host's settings. Used for clean removal on uninstall. | | `components` | `object` | Mirror of the manifest `components` object for quick reference. | | `remote_sources` | `object` | Map of component path to remote source metadata. Only present for packages with remote component references. Keys are component identifiers; values are objects with `url`, `checksum`, `fetched_at`, and `cache_ttl`. | diff --git a/src/content/docs/specification/component-types.mdx b/src/content/docs/specification/component-types.mdx index 46ed19e..72fd2a6 100644 --- a/src/content/docs/specification/component-types.mdx +++ b/src/content/docs/specification/component-types.mdx @@ -292,7 +292,7 @@ When installing a package that declares an MCP server already present in the hos - **key_name**: The key in the `mcpServers` object (e.g., `"context7"`). - **origin**: Derived from the server mode: - - Mode 1 (command+args): `command::{command} {args[0]}` (e.g., `command::npx -y @anthropic/context7-mcp`). + - Mode 1 (command+args): `command::{command} {args_joined}`, where `{args_joined}` is all elements of the `args` array joined with a single space. If `args` is empty or omitted, the origin is `command::{command}` (no trailing space). For example, `command: "npx"`, `args: ["-y", "@anthropic/context7-mcp"]` yields `command::npx -y @anthropic/context7-mcp`. - Mode 2 (embedded mcpb): `bundle::{bundle_path}` normalized to the archive-relative path. - Mode 3 (referenced mcpb): the `source` URL verbatim. @@ -302,7 +302,8 @@ Two servers are considered the same when both key_name and origin match. - Same identity, incoming version higher: replace. Re-render MCP config from the incoming package's template. - Same identity, incoming version equal or lower: skip rendering. Track in lockfile only. -- Same key_name, different origin: conflict. The installer MUST warn the user. In interactive mode, the installer SHOULD offer to keep the existing server, replace it, or install both under distinct keys. +- When comparing versions, treat `null`/unknown as lower than any concrete semver version. If both are `null`/unknown, keep the existing server (treat as equal). +- Same key_name, different origin: conflict. In interactive mode, the installer MUST warn the user and offer to keep the existing server, replace it, or install both under distinct keys. In non-interactive mode, the installer MUST fail with a descriptive error (implementations MAY support a preconfigured conflict policy). **User Override.** Deduplication is the default behavior. Installers MUST provide a mechanism for users to override deduplication: diff --git a/src/content/docs/specification/install-lifecycle.mdx b/src/content/docs/specification/install-lifecycle.mdx index b0d35d2..74cac10 100644 --- a/src/content/docs/specification/install-lifecycle.mdx +++ b/src/content/docs/specification/install-lifecycle.mdx @@ -64,13 +64,19 @@ sequenceDiagram b. If no matching entry exists in `shared_mcp_servers`: render the template, merge into the host config, and add the server to `shared_mcp_servers` with `declared_by` set to the current package. - c. If a match exists and the incoming version is higher: re-render using the incoming package's template, update `active_source` and `version`, and append the package to `declared_by`. + c. If a match exists, compare versions. Treat `null`/unknown versions as lower than any concrete semver version. If both are `null`/unknown, treat them as equal. - d. If a match exists and the incoming version is equal or lower: skip rendering and append the package to `declared_by` only. + - If the incoming version is higher: re-render using the incoming package's template, update `active_source` and `version`, and append the package to `declared_by`. - e. If the key_name matches but the origin differs: warn the user and offer resolution options (keep, replace, or install both under distinct keys). + - If the incoming version is equal or lower (including both `null`): skip rendering and append the package to `declared_by` only. - f. If the user has disabled dedup for this server (`dedup: false`), skip dedup checks and install the server as a separate entry. + d. If the key_name matches but the origin differs (conflict): + + - **Interactive mode**: warn the user and offer resolution options: keep the existing server, replace it, or install both under distinct keys. + + - **Non-interactive mode**: fail the install with a non-zero exit status and a descriptive error. Implementations MAY allow a preconfigured conflict policy (e.g., via CLI flags) to resolve without prompting. + + e. If the user has disabled dedup for this server (`dedup: false`), skip dedup checks and install the server under a package-scoped key (e.g., `{key_name}#{package_name}`). The installer MUST record the concrete key in the lockfile so uninstall can identify the correct entry. Rendered `.lsp.json` files are written to the install location without deduplication (LSP server dedup is deferred to a future spec version). @@ -89,7 +95,7 @@ Uninstalling a package reverses the install process: a. If this package is the only entry in the server's `declared_by` list: remove the server from the host config and from `shared_mcp_servers`. - b. If other packages remain in `declared_by`: remove this package from the list. If this package was the `active_source`, select the remaining package with the highest version, re-extract its MCP template from the archive cache, re-render with that package's config values, and update `active_source`. If this package was not the active source, no config change is needed. + b. If other packages remain in `declared_by`: remove this package from the list. If this package was the `active_source`, select the remaining package with the highest version, re-materialize its MCP template, re-render with that package's config values, and update `active_source`. For archive-backed packages, re-extract from the archive cache. For linked packages (`source: link:...`), read the template directly from the linked directory. If this package was not the active source, no config change is needed. c. If the server has `dedup: false`: remove only this package's copy. Other packages' copies are independent and unaffected. diff --git a/src/content/docs/specification/lockfile.mdx b/src/content/docs/specification/lockfile.mdx index 153c97d..1051190 100644 --- a/src/content/docs/specification/lockfile.mdx +++ b/src/content/docs/specification/lockfile.mdx @@ -96,8 +96,8 @@ Installers MUST maintain a local cache of installed `.ccpkg` archives to support | Scope | Cache Path | |---|---| -| User | `~/.ccpkg/cache/archives/{name}-{version}.ccpkg` | -| Project | `{project-root}/.ccpkg/cache/archives/{name}-{version}.ccpkg` | +| User | `~/.claude/ccpkg-cache/archives/{name}-{version}.ccpkg` | +| Project | `{project-root}/.claude/ccpkg-cache/archives/{name}-{version}.ccpkg` | ### Retention