feat: validate manifest version during pack command

Add validation to ensure the manifest_version matches the current expected
version (0.2) during the mcpb pack command. This prevents users from
packing extensions with outdated or incorrect manifest versions.

Changes:
- Add CURRENT_MANIFEST_VERSION constant to centralize version management
- Validate manifest version in pack command before creating bundle
- Update init command to use version constant
- Update tests to use version constant
- Fix prettier formatting issues
This commit is contained in:
Joan Xie 2025-09-22 14:43:28 -07:00
commit f92fe4ab69
No known key found for this signature in database
9 changed files with 70 additions and 47 deletions

View file

@ -3,6 +3,7 @@
> **⚠️ IMPORTANT NOTICE: This project is being renamed from DXT (Desktop Extensions) to MCPB (MCP Bundles)** > **⚠️ IMPORTANT NOTICE: This project is being renamed from DXT (Desktop Extensions) to MCPB (MCP Bundles)**
> >
> If you're looking for the DXT tools, they have been renamed to MCPB. Please update your dependencies and tooling: > If you're looking for the DXT tools, they have been renamed to MCPB. Please update your dependencies and tooling:
>
> - `dxt` CLI is now `mcpb` > - `dxt` CLI is now `mcpb`
> - `.dxt` files are now `.mcpb` files > - `.dxt` files are now `.mcpb` files
> - `@anthropic-ai/dxt` package will be moved to `@anthropic-ai/mcpb` > - `@anthropic-ai/dxt` package will be moved to `@anthropic-ai/mcpb`

View file

@ -1,4 +1,7 @@
import { McpbManifestSchema, McpbSignatureInfoSchema } from "../dist/schemas.js"; import {
McpbManifestSchema,
McpbSignatureInfoSchema,
} from "../dist/schemas.js";
import * as z from "zod/v4"; import * as z from "zod/v4";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";

View file

@ -2,6 +2,7 @@ import { confirm, input, select } from "@inquirer/prompts";
import { existsSync, readFileSync, writeFileSync } from "fs"; import { existsSync, readFileSync, writeFileSync } from "fs";
import { basename, join, resolve } from "path"; import { basename, join, resolve } from "path";
import { CURRENT_MANIFEST_VERSION } from "../schemas.js";
import type { McpbManifest } from "../types.js"; import type { McpbManifest } from "../types.js";
interface PackageJson { interface PackageJson {
@ -773,7 +774,7 @@ export function buildManifest(
const { keywords, license, repository } = optionalFields; const { keywords, license, repository } = optionalFields;
return { return {
manifest_version: "0.1", manifest_version: CURRENT_MANIFEST_VERSION,
name, name,
...(displayName && displayName !== name ...(displayName && displayName !== name
? { display_name: displayName } ? { display_name: displayName }

View file

@ -13,7 +13,7 @@ import { basename, join, relative, resolve, sep } from "path";
import { getAllFilesWithCount, readMcpbIgnorePatterns } from "../node/files.js"; import { getAllFilesWithCount, readMcpbIgnorePatterns } from "../node/files.js";
import { validateManifest } from "../node/validate.js"; import { validateManifest } from "../node/validate.js";
import { McpbManifestSchema } from "../schemas.js"; import { CURRENT_MANIFEST_VERSION, McpbManifestSchema } from "../schemas.js";
import { getLogger } from "../shared/log.js"; import { getLogger } from "../shared/log.js";
import { initExtension } from "./init.js"; import { initExtension } from "./init.js";
@ -101,6 +101,17 @@ export async function packExtension({
return false; return false;
} }
const manifestVersion = manifest.manifest_version || manifest.dxt_version;
if (manifestVersion !== CURRENT_MANIFEST_VERSION) {
logger.error(
`ERROR: Manifest version mismatch. Expected "${CURRENT_MANIFEST_VERSION}", found "${manifestVersion}"`,
);
logger.error(
` Please update the manifest_version in your manifest.json to "${CURRENT_MANIFEST_VERSION}"`,
);
return false;
}
// Determine output path // Determine output path
const extensionName = basename(resolvedPath); const extensionName = basename(resolvedPath);
const finalOutputPath = outputPath const finalOutputPath = outputPath

View file

@ -79,38 +79,39 @@ export const McpbUserConfigValuesSchema = z.record(
export const McpbManifestSchema = z export const McpbManifestSchema = z
.object({ .object({
$schema: z.string().optional(), $schema: z.string().optional(),
dxt_version: z.string().optional().describe("@deprecated Use manifest_version instead"), dxt_version: z
.string()
.optional()
.describe("@deprecated Use manifest_version instead"),
manifest_version: z.string().optional(), manifest_version: z.string().optional(),
name: z.string(), name: z.string(),
display_name: z.string().optional(), display_name: z.string().optional(),
version: z.string(), version: z.string(),
description: z.string(), description: z.string(),
long_description: z.string().optional(), long_description: z.string().optional(),
author: McpbManifestAuthorSchema, author: McpbManifestAuthorSchema,
repository: McpbManifestRepositorySchema.optional(), repository: McpbManifestRepositorySchema.optional(),
homepage: z.string().url().optional(), homepage: z.string().url().optional(),
documentation: z.string().url().optional(), documentation: z.string().url().optional(),
support: z.string().url().optional(), support: z.string().url().optional(),
icon: z.string().optional(), icon: z.string().optional(),
screenshots: z.array(z.string()).optional(), screenshots: z.array(z.string()).optional(),
server: McpbManifestServerSchema, server: McpbManifestServerSchema,
tools: z.array(McpbManifestToolSchema).optional(), tools: z.array(McpbManifestToolSchema).optional(),
tools_generated: z.boolean().optional(), tools_generated: z.boolean().optional(),
prompts: z.array(McpbManifestPromptSchema).optional(), prompts: z.array(McpbManifestPromptSchema).optional(),
prompts_generated: z.boolean().optional(), prompts_generated: z.boolean().optional(),
keywords: z.array(z.string()).optional(), keywords: z.array(z.string()).optional(),
license: z.string().optional(), license: z.string().optional(),
compatibility: McpbManifestCompatibilitySchema.optional(), compatibility: McpbManifestCompatibilitySchema.optional(),
user_config: z user_config: z
.record(z.string(), McpbUserConfigurationOptionSchema) .record(z.string(), McpbUserConfigurationOptionSchema)
.optional(), .optional(),
}) })
.refine( .refine((data) => !!(data.dxt_version || data.manifest_version), {
(data) => !!(data.dxt_version || data.manifest_version), message:
{ "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided",
message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", });
}
);
export const McpbSignatureInfoSchema = z.object({ export const McpbSignatureInfoSchema = z.object({
status: z.enum(["signed", "unsigned", "self-signed"]), status: z.enum(["signed", "unsigned", "self-signed"]),

View file

@ -1,5 +1,7 @@
import * as z from "zod"; import * as z from "zod";
export const CURRENT_MANIFEST_VERSION = "0.2";
export const McpServerConfigSchema = z.strictObject({ export const McpServerConfigSchema = z.strictObject({
command: z.string(), command: z.string(),
args: z.array(z.string()).optional(), args: z.array(z.string()).optional(),
@ -79,7 +81,10 @@ export const McpbUserConfigValuesSchema = z.record(
export const McpbManifestSchema = z export const McpbManifestSchema = z
.strictObject({ .strictObject({
$schema: z.string().optional(), $schema: z.string().optional(),
dxt_version: z.string().optional().describe("@deprecated Use manifest_version instead"), dxt_version: z
.string()
.optional()
.describe("@deprecated Use manifest_version instead"),
manifest_version: z.string().optional(), manifest_version: z.string().optional(),
name: z.string(), name: z.string(),
display_name: z.string().optional(), display_name: z.string().optional(),
@ -106,12 +111,10 @@ export const McpbManifestSchema = z
.record(z.string(), McpbUserConfigurationOptionSchema) .record(z.string(), McpbUserConfigurationOptionSchema)
.optional(), .optional(),
}) })
.refine( .refine((data) => !!(data.dxt_version || data.manifest_version), {
(data) => !!(data.dxt_version || data.manifest_version), message:
{ "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided",
message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", });
}
);
export const McpbSignatureInfoSchema = z.strictObject({ export const McpbSignatureInfoSchema = z.strictObject({
status: z.enum(["signed", "unsigned", "self-signed"]), status: z.enum(["signed", "unsigned", "self-signed"]),

View file

@ -2,6 +2,8 @@ import { execSync } from "node:child_process";
import fs from "node:fs"; import fs from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { CURRENT_MANIFEST_VERSION } from "../src/schemas.js";
interface ExecSyncError extends Error { interface ExecSyncError extends Error {
stdout: Buffer; stdout: Buffer;
stderr: Buffer; stderr: Buffer;
@ -91,7 +93,7 @@ describe("DXT CLI", () => {
fs.writeFileSync( fs.writeFileSync(
join(tempDir, "manifest.json"), join(tempDir, "manifest.json"),
JSON.stringify({ JSON.stringify({
manifest_version: "1.0", manifest_version: CURRENT_MANIFEST_VERSION,
name: "Test Extension", name: "Test Extension",
version: "1.0.0", version: "1.0.0",
description: "A test extension", description: "A test extension",
@ -188,7 +190,7 @@ describe("DXT CLI", () => {
fs.writeFileSync( fs.writeFileSync(
join(tempExecDir, "manifest.json"), join(tempExecDir, "manifest.json"),
JSON.stringify({ JSON.stringify({
manifest_version: "1.0", manifest_version: CURRENT_MANIFEST_VERSION,
name: "Test Executable Extension", name: "Test Executable Extension",
version: "1.0.0", version: "1.0.0",
description: "A test extension with executable files", description: "A test extension with executable files",

View file

@ -10,6 +10,7 @@ import {
getDefaultRepositoryUrl, getDefaultRepositoryUrl,
readPackageJson, readPackageJson,
} from "../src/cli/init.js"; } from "../src/cli/init.js";
import { CURRENT_MANIFEST_VERSION } from "../src/schemas.js";
// Mock the fs module // Mock the fs module
jest.mock("fs", () => ({ jest.mock("fs", () => ({
@ -217,7 +218,7 @@ describe("init functions", () => {
); );
expect(manifest).toEqual({ expect(manifest).toEqual({
manifest_version: "0.1", manifest_version: CURRENT_MANIFEST_VERSION,
name: "test-extension", name: "test-extension",
version: "1.0.0", version: "1.0.0",
description: "Test description", description: "Test description",
@ -303,7 +304,7 @@ describe("init functions", () => {
); );
expect(manifest).toEqual({ expect(manifest).toEqual({
manifest_version: "0.1", manifest_version: CURRENT_MANIFEST_VERSION,
name: "test-extension", name: "test-extension",
display_name: "Test Extension", display_name: "Test Extension",
version: "1.0.0", version: "1.0.0",