feat(pack): add mcpb bundling support with cvm manifest conventions#4
feat(pack): add mcpb bundling support with cvm manifest conventions#4abhayguptas wants to merge 8 commits into
Conversation
- Add transport field (stdio/cvm) for dual-mode bundle execution - Add env_mapping contract for native CVM server config injection - Restructure CVM meta to use nested defaults object - Add docker server type with image and compose_file support - Implement CVM transport mode in serve (spawn with env vars, no Gateway) - Update pack-init wizard with transport, docker, and env_mapping prompts - Remove announce/pricing fields (not needed for initial scope) - Fix duplicate --help line in serve help text
47192d5 to
c8dd4c0
Compare
- Fix encryption enum: use 'required' instead of 'nip44' to match SDK EncryptionMode - Make display_name optional per MCPB spec - Add mcp_config.env support with __dirname substitution - Add 'uv' server type for Python UV dependency management (MCPB v0.4+)
c8dd4c0 to
457b8df
Compare
|
This PR adds 🔴 CRITICAL1. Mismatched
|
| Call site | Ignore list |
|---|---|
pack.ts:72-78 |
['.git', 'node_modules', '.DS_Store', '.env', '.cvmb'] |
serve.ts:150-157 |
['.git', 'node_modules', '.DS_Store', '.env', '.cvmb', '.mcpb'] |
If a source directory contains any .mcpb file, the pack-time hash will include it in the Merkle tree, but the verify-time hash will exclude it. The hashes won't match, and cvmi serve will reject a valid bundle with:
Content hash verification failed! The bundle contents have been modified.
Fix: Extract the ignore list to a single shared constant and use it in both locations, or at minimum add '.mcpb' to the pack-time ignore list.
2. cvmi pack destructively modifies the user's source manifest.json
pack.ts:127 writes the manifest with injected _meta.com.contextvm.content_hash and _sig back to disk:
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));This permanently mutates the source file before archiving. If the user's manifest.json is version-controlled, every cvmi pack produces a dirty working tree. The content hash and signature should only exist inside the ZIP archive, not in the source copy.
Fix: Add the signed manifest to the archive from an in-memory buffer instead of writing it to disk first. The archive phase already uses archiver, which supports adding content from strings/buffers via archive.append().
3. Redundant @noble/curves direct dependency
package.json:98 declares:
"@noble/curves": "^2.2.0",nostr-tools (^2.23.3) already depends on and re-exports @noble/curves internally. Adding it as a direct dependency:
- Duplicates a transitive dependency in the lockfile
- Risks version mismatches (e.g.,
nostr-toolsexpecting@noble/curves@2.1.xbut hoisted to2.2.x) - Adds unnecessary weight to
node_modules
src/pack/crypto.ts:4 uses schnorr.sign() and schnorr.verify() from the noble import, but nostr-tools already exposes signing via its finalizeEvent / getSignature APIs. Alternatively, the Schnorr primitives can be accessed from nostr-tools's transitive dependency without listing it directly.
Fix: Remove @noble/curves from dependencies and use nostr-tools's signing utilities, or at minimum remove it from direct deps since it's already available transitively.
🟠 HIGH
4. Hash ignores entire node_modules/ but archive bundles it (minus .cache)
The content hash explicitly excludes all of node_modules (per spec), but the archive includes node_modules/ (only excluding .cache). This means:
- The hash does not cover
node_modules/at all - A tampered
node_modules/in a.cvmbbundle would pass verification undetected
The spec (line 329) says to exclude node_modules from the hash — so this is technically spec-compliant. However, it's a security consideration worth flagging: the bundle's trust boundary stops at package.json/pyproject.toml but doesn't extend to the actual dependency code.
Recommendation: Add a comment documenting that node_modules integrity relies on package-lock.json/pnpm-lock.yaml/yarn.lock being in the hash, and that users should audit dependencies separately. Or consider including node_modules in the hash for node type servers.
5. manifest_version: '1.0' in pack-init.ts is misleading
pack-init.ts:162 hardcodes:
manifest_version: '1.0',But the spec is still a draft with version "0.3" (spec line 48). Using "1.0" implies a stable, finalized specification that doesn't exist yet. The MCPB schema files shipped in the repo (e.g., mcpb/schemas/mcpb-manifest-v0.3.schema.json) follow 0.x versioning convention.
Fix: Use "0.3" or track the actual spec version.
6. description is required per spec but optional in zod schema
Spec line 51 lists description as a required field. But cvm-manifest.ts:38 declares:
description: z.string().optional(),This silently allows bundles with no description through validation.
🟡 MEDIUM
7. No semver validation on version
cvm-manifest.ts:37 accepts any string:
version: z.string(),The spec (line 50) says "Semantic version (semver)". Invalid version strings ("latest", "abc") pass validation.
Fix: Add .regex(/^\d+\.\d+\.\d+/) or use a semver parsing refinement.
8. Regex injection risk in user_config variable substitution
serve.ts:222-225 builds a RegExp from unescaped config key names:
new RegExp(`\\$\\{user_config\\.${key}\\}`, 'g')If a user_config key contains regex metacharacters (., $, *, etc.), the regex will behave unexpectedly. For example, a key named api.key would match ${user_config.apiXkey} due to the unescaped ..
Fix: Escape the key before inserting it into the regex pattern.
9. No tests for the pack/crypto/manifest modules
There are zero test files for:
src/pack.ts— pack orchestration logicsrc/pack/crypto.ts— canonicalization, signing, verification, Merkle hashingsrc/pack/cvm-manifest.ts— manifest validationsrc/pack/extract.ts— ZIP extraction and manifest loading
The sign/verify and hash computation logic is cryptographic in nature and strongly needs test coverage — especially given the mismatched ignore list bug above.
10. Dynamic import() in serve.ts:144-145 is unnecessary
const { computeDirectoryContentHash, verifyManifestSignature } =
await import('./pack/crypto.ts');This dynamic import has no conditionality — it's inside an if (target.endsWith('.cvmb')) block but always executed when the condition is true. A static import at the top of the file would work identically and be simpler.
🟢 LOW / CODE SMELLS
11. Magic ignore-list arrays duplicated across files
The file/directory ignore patterns appear in three places, each slightly different:
| Location | Patterns |
|---|---|
pack.ts:72-78 (hash) |
.git, node_modules, .DS_Store, .env, .cvmb |
pack.ts:151-159 (archive glob) |
.git/**, node_modules/.cache/**, .DS_Store, .env, *.cvmb, *.mcpb, outFileName |
serve.ts:150-157 (hash verify) |
.git, node_modules, .DS_Store, .env, .cvmb, .mcpb |
crypto.ts:91 (function default) |
.git, node_modules, .DS_Store, .env |
Extract a shared const BUNDLE_IGNORE_PATTERNS and a separate const CONTENT_HASH_IGNORE_PATTERNS to a constants file.
12. ignoreList uses relPath.includes() which is too broad
if (relPath.includes(ignore) || relPath.startsWith(ignore)) return false;.includes() for .git would also match my-strategy.ts (contains "git"). .startsWith() handles the directory case. Use path-segment-aware matching instead.
13. pack-init.ts hardcodes manifest_version: '1.0' rather than referencing a constant
The spec version string should live in a single exported constant that both pack-init.ts and cvm-manifest.ts reference.
14. extractBundle() parameter named mcpbPath but function handles .cvmb
extract.ts:9 — the parameter name mcpbPath is a leftover from the MCPB heritage. Should be bundlePath or cvmbPath.
15. user_config example in pack-init.ts injects CVM_RELAYS env var unconditionally
pack-init.ts:144-146 always adds:
mcpConfig.env = {
CVM_RELAYS: '${user_config.relays}',
};This overwrites any env that might have been set for mcp_config and is CVM-specific. It should only be added for transport: 'cvm' or be configurable.
✅ What's Good
- Spec compliance: The packing flow (validate → hash → sign → archive) and verification flow (extract → verify hash → verify sig → resolve config → spawn) correctly implement the spec's 4-phase pipeline
- Canonicalization: Correct use of
canonicalize(RFC 8785) with_sigremoval before canonicalization - Schnorr signing:
_sigstructure matches the spec exactly (pubkey,id,signature,created_at) - User config prompting:
serve.tscorrectly iteratesuser_config, checks env vars first, differentiatesbooleanvs text prompts, and handlessensitive: truewithp.password - Variable substitution:
${__dirname}and${user_config.X}resolution works in bothcommand/argsandenv - Interactive
pack init: The wizard covers all server types with sensible defaults - Transport support: Both
stdio(Gateway) andcvm(native) transports are properly handled inserve.ts
Core fix: nostr-tools-only signing (removed
|
|
Awesome work! Everything looks perfect. I've pulled the latest commit locally and verified that the build is clean without the I also verified that the |
|
Cool work! Switching to native There are a few leftovers or hygiene items that i thought should be cleaned up before merging (lmk your thoughts on these)
Some further improvements we could consider:
|
Resolves #3
Description
This PR introduces the
packcommand, enabling developers to package local MCP servers into distributable.mcpbbundles. It also extends theservecommand to seamlessly execute these bundles over the Nostr network.The implementation fully adheres to Anthropic's MCPB specification while extending it with a
_meta.com.contextvmnamespace to carry CVM-specific metadata (relays, public mode, encryption, etc.) without breaking compatibility with other hosts.Changes Made
src/pack/cvm-manifest.ts) to strictly type and validate the_meta.com.contextvmnamespace insidemanifest.json.cvmi pack: Introduced a new subcommand that packages a project directory into an.mcpbzip archive with maximum compression.cvmi packand no manifest exists, an interactive wizard prompts them for server details and CVM-specific defaults (relays, encryption mode) to generate a validmanifest.json.cvmi serve <bundle.mcpb>: Upgraded the serve command to accept an.mcpbfile. It extracts the bundle into a secure temporary directory, reads the custom metadata to override gateway defaults, and runs the server over Nostr.cli.tsand updatedAGENTS.mddocs.Verification
.git,.DS_Store, andnode_modules/.cachewhile preserving necessary dotfiles (dot: true).cvmi servecorrectly extracts the bundle, parses the CVM config, applies runtime overrides via CLI flags, and successfully exposes the server.pnpm type-checkwith 0 errors.