Skip to content

Commit bc298d8

Browse files
committed
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
1 parent 2e4d6c4 commit bc298d8

4 files changed

Lines changed: 31 additions & 17 deletions

File tree

spec/specification.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ When installing a package that declares an MCP server already present in the hos
807807

808808
- **key_name**: The key in the `mcpServers` object (e.g., `"context7"`).
809809
- **origin**: Derived from the server mode:
810-
- Mode 1 (command+args): `command::{command} {args[0]}` (e.g., `command::npx -y @anthropic/context7-mcp`).
810+
- 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`.
811811
- Mode 2 (embedded mcpb): `bundle::{bundle_path}` normalized to the archive-relative path.
812812
- Mode 3 (referenced mcpb): the `source` URL verbatim.
813813

@@ -817,7 +817,8 @@ Two servers are considered the same when both key_name and origin match.
817817

818818
- Same identity, incoming version higher: replace. Re-render MCP config from the incoming package's template.
819819
- Same identity, incoming version equal or lower: skip rendering. Track in lockfile only.
820-
- 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.
820+
- 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).
821+
- 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).
821822

822823
**User Override.** Deduplication is the default behavior. Installers MUST provide a mechanism for users to override deduplication:
823824

@@ -1059,13 +1060,19 @@ sequenceDiagram
10591060

10601061
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.
10611062

1062-
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`.
1063+
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.
10631064

1064-
d. If a match exists and the incoming version is equal or lower: skip rendering and append the package to `declared_by` only.
1065+
- 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`.
10651066

1066-
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).
1067+
- If the incoming version is equal or lower (including both `null`): skip rendering and append the package to `declared_by` only.
10671068

1068-
f. If the user has disabled dedup for this server (`dedup: false`), skip dedup checks and install the server as a separate entry.
1069+
d. If the key_name matches but the origin differs (conflict):
1070+
1071+
- **Interactive mode**: warn the user and offer resolution options: keep the existing server, replace it, or install both under distinct keys.
1072+
1073+
- **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.
1074+
1075+
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.
10691076

10701077
Rendered `.lsp.json` files are written to the install location without deduplication (LSP server dedup is deferred to a future spec version).
10711078

@@ -1089,7 +1096,7 @@ Uninstalling a package reverses the install process:
10891096

10901097
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`.
10911098

1092-
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.
1099+
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.
10931100

10941101
c. If the server has `dedup: false`: remove only this package's copy. Other packages' copies are independent and unaffected.
10951102
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
13081315
| `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. |
13091316
| `enabled_plugins_key` | `string` | The key written to the host's `enabledPlugins` (e.g., `"api-testing@ccpkg"`). Used for clean deregistration on uninstall. |
13101317
| `installed_files` | `string[]` | List of all files written during install, relative to the package directory. Enables deterministic uninstall. Empty for linked packages. |
1311-
| `merged_mcp_servers` | `string[]` | MCP server names merged into the host's `.mcp.json` during install. Used for clean removal on uninstall. |
1318+
| `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). |
13121319
| `config_keys` | `string[]` | Config variable names stored in the host's settings. Used for clean removal on uninstall. |
13131320
| `components` | `object` | Mirror of the manifest `components` object for quick reference. |
13141321
| `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`. |

src/content/docs/specification/component-types.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ When installing a package that declares an MCP server already present in the hos
292292

293293
- **key_name**: The key in the `mcpServers` object (e.g., `"context7"`).
294294
- **origin**: Derived from the server mode:
295-
- Mode 1 (command+args): `command::{command} {args[0]}` (e.g., `command::npx -y @anthropic/context7-mcp`).
295+
- 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`.
296296
- Mode 2 (embedded mcpb): `bundle::{bundle_path}` normalized to the archive-relative path.
297297
- Mode 3 (referenced mcpb): the `source` URL verbatim.
298298

@@ -302,7 +302,8 @@ Two servers are considered the same when both key_name and origin match.
302302

303303
- Same identity, incoming version higher: replace. Re-render MCP config from the incoming package's template.
304304
- Same identity, incoming version equal or lower: skip rendering. Track in lockfile only.
305-
- 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.
305+
- 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).
306+
- 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).
306307

307308
**User Override.** Deduplication is the default behavior. Installers MUST provide a mechanism for users to override deduplication:
308309

src/content/docs/specification/install-lifecycle.mdx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,19 @@ sequenceDiagram
6464

6565
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.
6666

67-
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`.
67+
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.
6868

69-
d. If a match exists and the incoming version is equal or lower: skip rendering and append the package to `declared_by` only.
69+
- 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`.
7070

71-
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).
71+
- If the incoming version is equal or lower (including both `null`): skip rendering and append the package to `declared_by` only.
7272

73-
f. If the user has disabled dedup for this server (`dedup: false`), skip dedup checks and install the server as a separate entry.
73+
d. If the key_name matches but the origin differs (conflict):
74+
75+
- **Interactive mode**: warn the user and offer resolution options: keep the existing server, replace it, or install both under distinct keys.
76+
77+
- **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.
78+
79+
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.
7480

7581
Rendered `.lsp.json` files are written to the install location without deduplication (LSP server dedup is deferred to a future spec version).
7682

@@ -89,7 +95,7 @@ Uninstalling a package reverses the install process:
8995

9096
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`.
9197

92-
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.
98+
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.
9399

94100
c. If the server has `dedup: false`: remove only this package's copy. Other packages' copies are independent and unaffected.
95101

src/content/docs/specification/lockfile.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ Installers MUST maintain a local cache of installed `.ccpkg` archives to support
9696

9797
| Scope | Cache Path |
9898
|---|---|
99-
| User | `~/.ccpkg/cache/archives/{name}-{version}.ccpkg` |
100-
| Project | `{project-root}/.ccpkg/cache/archives/{name}-{version}.ccpkg` |
99+
| User | `~/.claude/ccpkg-cache/archives/{name}-{version}.ccpkg` |
100+
| Project | `{project-root}/.claude/ccpkg-cache/archives/{name}-{version}.ccpkg` |
101101

102102
### Retention
103103

0 commit comments

Comments
 (0)