From 39fe8034799318d78cefd18e0bbe6fc76514362b Mon Sep 17 00:00:00 2001 From: guyru Date: Mon, 22 Sep 2025 17:19:05 +0300 Subject: [PATCH 01/55] The repository url should actually point to the git repo In the example, the repo URL should actually point to the git repo and not the github project. --- MANIFEST.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.md b/MANIFEST.md index eb8ee46..30f41cd 100644 --- a/MANIFEST.md +++ b/MANIFEST.md @@ -84,7 +84,7 @@ A full `manifest.json` with most of the optional fields looks like this: }, "repository": { "type": "git", - "url": "https://github.com/your-username/my-mcp-extension" + "url": "https://github.com/your-username/my-mcp-extension.git" }, "homepage": "https://example.com/my-extension", "documentation": "https://docs.example.com/my-extension", From 98126b777e43c9eead035dfd5f937da988a15b6d Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 1 Nov 2025 12:31:25 -0700 Subject: [PATCH 02/55] feat: use 0.2 as the default schema (#145) --- package.json | 2 +- src/cli/init.ts | 8 ++++---- src/cli/pack.ts | 30 +++++++++++++--------------- src/node/validate.ts | 19 ++++++++++++++---- src/shared/constants.ts | 8 ++++++++ src/shared/manifestVersionResolve.ts | 24 ++++++++++++++++++++++ test/cli.test.ts | 10 +++++----- test/init.test.ts | 6 +++--- 8 files changed, 74 insertions(+), 33 deletions(-) create mode 100644 src/shared/manifestVersionResolve.ts diff --git a/package.json b/package.json index 8749e20..77f437a 100644 --- a/package.json +++ b/package.json @@ -99,4 +99,4 @@ "@babel/parser": "7.27.3" }, "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f" -} \ No newline at end of file +} diff --git a/src/cli/init.ts b/src/cli/init.ts index 58a7317..7682e0f 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -2,8 +2,8 @@ import { confirm, input, select } from "@inquirer/prompts"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join, resolve } from "path"; -import { LATEST_MANIFEST_VERSION } from "../shared/constants.js"; -import type { McpbManifestLatest } from "../types.js"; +import { DEFAULT_MANIFEST_VERSION } from "../shared/constants.js"; +import type { McpbManifest } from "../types.js"; interface PackageJson { name?: string; @@ -878,14 +878,14 @@ export function buildManifest( resources: string; default_locale: string; }, -): McpbManifestLatest { +): McpbManifest { const { name, displayName, version, description, authorName } = basicInfo; const { authorEmail, authorUrl } = authorInfo; const { serverType, entryPoint, mcp_config } = serverConfig; const { keywords, license, repository } = optionalFields; return { - manifest_version: LATEST_MANIFEST_VERSION, + manifest_version: DEFAULT_MANIFEST_VERSION, name, ...(displayName && displayName !== name ? { display_name: displayName } diff --git a/src/cli/pack.ts b/src/cli/pack.ts index 58b3c1c..e2411bf 100644 --- a/src/cli/pack.ts +++ b/src/cli/pack.ts @@ -13,11 +13,9 @@ import { basename, join, relative, resolve, sep } from "path"; import { getAllFilesWithCount, readMcpbIgnorePatterns } from "../node/files.js"; import { validateManifest } from "../node/validate.js"; -import { - LATEST_MANIFEST_SCHEMA, - LATEST_MANIFEST_VERSION, -} from "../shared/constants.js"; +import { MANIFEST_SCHEMAS } from "../shared/constants.js"; import { getLogger } from "../shared/log.js"; +import { getManifestVersionFromRawData } from "../shared/manifestVersionResolve.js"; import { initExtension } from "./init.js"; interface PackOptions { @@ -95,7 +93,18 @@ export async function packExtension({ try { const manifestContent = readFileSync(manifestPath, "utf-8"); const manifestData = JSON.parse(manifestContent); - manifest = LATEST_MANIFEST_SCHEMA.parse(manifestData); + const manifestVersion = getManifestVersionFromRawData(manifestData); + if (!manifestVersion) { + logger.error( + `ERROR: Manifest version mismatch. Expected "${Object.keys(MANIFEST_SCHEMAS).join(" or ")}", found "${manifestVersion}"`, + ); + logger.error( + ` Please update the manifest_version in your manifest.json to a supported version`, + ); + return false; + } + + manifest = MANIFEST_SCHEMAS[manifestVersion].parse(manifestData); } catch (error) { logger.error("ERROR: Failed to parse manifest.json"); if (error instanceof Error) { @@ -104,17 +113,6 @@ export async function packExtension({ return false; } - const manifestVersion = manifest.manifest_version || manifest.dxt_version; - if (manifestVersion !== LATEST_MANIFEST_VERSION) { - logger.error( - `ERROR: Manifest version mismatch. Expected "${LATEST_MANIFEST_VERSION}", found "${manifestVersion}"`, - ); - logger.error( - ` Please update the manifest_version in your manifest.json to "${LATEST_MANIFEST_VERSION}"`, - ); - return false; - } - // Determine output path const extensionName = basename(resolvedPath); const finalOutputPath = outputPath diff --git a/src/node/validate.ts b/src/node/validate.ts index ac21747..d1ee8fd 100644 --- a/src/node/validate.ts +++ b/src/node/validate.ts @@ -7,9 +7,10 @@ import prettyBytes from "pretty-bytes"; import { unpackExtension } from "../cli/unpack.js"; import { - LATEST_MANIFEST_SCHEMA, - LATEST_MANIFEST_SCHEMA_LOOSE, + MANIFEST_SCHEMAS, + MANIFEST_SCHEMAS_LOOSE, } from "../shared/constants.js"; +import { getManifestVersionFromRawData } from "../shared/manifestVersionResolve.js"; /** * Check if a buffer contains a valid PNG file signature @@ -119,8 +120,13 @@ export function validateManifest(inputPath: string): boolean { const manifestContent = readFileSync(manifestPath, "utf-8"); const manifestData = JSON.parse(manifestContent); + const manifestVersion = getManifestVersionFromRawData(manifestData); + if (!manifestVersion) { + console.log("Unrecognized or unsupported manifest version"); + return false; + } - const result = LATEST_MANIFEST_SCHEMA.safeParse(manifestData); + const result = MANIFEST_SCHEMAS[manifestVersion].safeParse(manifestData); if (result.success) { console.log("Manifest schema validation passes!"); @@ -192,7 +198,12 @@ export async function cleanMcpb(inputPath: string) { const manifestPath = resolve(unpackPath, "manifest.json"); const originalManifest = await fs.readFile(manifestPath, "utf-8"); const manifestData = JSON.parse(originalManifest); - const result = LATEST_MANIFEST_SCHEMA_LOOSE.safeParse(manifestData); + const manifestVersion = getManifestVersionFromRawData(manifestData); + if (!manifestVersion) { + throw new Error("Unrecognized or unsupported manifest version"); + } + const result = + MANIFEST_SCHEMAS_LOOSE[manifestVersion].safeParse(manifestData); if (!result.success) { throw new Error( diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 95a5cf4..6d16f5d 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -9,9 +9,15 @@ import { McpbManifestSchema as CurrentLooseManifestSchema } from "../schemas_loo /** * Latest manifest version - the version that new manifests should use + * @deprecated */ export const LATEST_MANIFEST_VERSION = "0.3" as const; +/** + * Default manifest version for new packages + */ +export const DEFAULT_MANIFEST_VERSION = "0.2" as const; + /** * Map of manifest versions to their strict schemas */ @@ -32,10 +38,12 @@ export const MANIFEST_SCHEMAS_LOOSE = { /** * Get the latest manifest schema based on LATEST_MANIFEST_VERSION + * @deprecated */ export const LATEST_MANIFEST_SCHEMA = CurrentManifestSchema; /** * Get the latest loose manifest schema based on LATEST_MANIFEST_VERSION + * @deprecated */ export const LATEST_MANIFEST_SCHEMA_LOOSE = CurrentLooseManifestSchema; diff --git a/src/shared/manifestVersionResolve.ts b/src/shared/manifestVersionResolve.ts new file mode 100644 index 0000000..5bc6381 --- /dev/null +++ b/src/shared/manifestVersionResolve.ts @@ -0,0 +1,24 @@ +import { MANIFEST_SCHEMAS } from "./constants.js"; + +export function getManifestVersionFromRawData(manifestData: unknown) { + let manifestVersion: keyof typeof MANIFEST_SCHEMAS | null = null; + if ( + typeof manifestData === "object" && + manifestData && + "manifest_version" in manifestData && + typeof manifestData.manifest_version === "string" && + Object.keys(MANIFEST_SCHEMAS).includes(manifestData.manifest_version) + ) { + manifestVersion = + manifestData.manifest_version as keyof typeof MANIFEST_SCHEMAS; + } else if ( + typeof manifestData === "object" && + manifestData && + "dxt_version" in manifestData && + typeof manifestData.dxt_version === "string" && + Object.keys(MANIFEST_SCHEMAS).includes(manifestData.dxt_version) + ) { + manifestVersion = manifestData.dxt_version as keyof typeof MANIFEST_SCHEMAS; + } + return manifestVersion; +} diff --git a/test/cli.test.ts b/test/cli.test.ts index 9bdc1eb..e7757ac 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -2,7 +2,7 @@ import { execSync } from "node:child_process"; import fs from "node:fs"; import { join } from "node:path"; -import { LATEST_MANIFEST_VERSION } from "../src/schemas/index.js"; +import { DEFAULT_MANIFEST_VERSION } from "../src/shared/constants"; interface ExecSyncError extends Error { stdout: Buffer; @@ -113,7 +113,7 @@ describe("DXT CLI", () => { fs.writeFileSync( manifestWithIcon, JSON.stringify({ - manifest_version: LATEST_MANIFEST_VERSION, + manifest_version: DEFAULT_MANIFEST_VERSION, name: "test-with-icon", version: "1.0.0", description: "Test with icon", @@ -140,7 +140,7 @@ describe("DXT CLI", () => { fs.writeFileSync( manifestWithUrl, JSON.stringify({ - manifest_version: LATEST_MANIFEST_VERSION, + manifest_version: DEFAULT_MANIFEST_VERSION, name: "test-with-url", version: "1.0.0", description: "Test with URL", @@ -177,7 +177,7 @@ describe("DXT CLI", () => { fs.writeFileSync( join(tempDir, "manifest.json"), JSON.stringify({ - manifest_version: LATEST_MANIFEST_VERSION, + manifest_version: DEFAULT_MANIFEST_VERSION, name: "Test Extension", version: "1.0.0", description: "A test extension", @@ -274,7 +274,7 @@ describe("DXT CLI", () => { fs.writeFileSync( join(tempExecDir, "manifest.json"), JSON.stringify({ - manifest_version: LATEST_MANIFEST_VERSION, + manifest_version: DEFAULT_MANIFEST_VERSION, name: "Test Executable Extension", version: "1.0.0", description: "A test extension with executable files", diff --git a/test/init.test.ts b/test/init.test.ts index caf598f..a9b9bf4 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -10,7 +10,7 @@ import { getDefaultRepositoryUrl, readPackageJson, } from "../src/cli/init.js"; -import { LATEST_MANIFEST_VERSION } from "../src/schemas/index.js"; +import { DEFAULT_MANIFEST_VERSION } from "../src/shared/constants.js"; // Mock the fs module jest.mock("fs", () => ({ @@ -220,7 +220,7 @@ describe("init functions", () => { ); expect(manifest).toEqual({ - manifest_version: LATEST_MANIFEST_VERSION, + manifest_version: DEFAULT_MANIFEST_VERSION, name: "test-extension", version: "1.0.0", description: "Test description", @@ -322,7 +322,7 @@ describe("init functions", () => { ); expect(manifest).toEqual({ - manifest_version: LATEST_MANIFEST_VERSION, + manifest_version: DEFAULT_MANIFEST_VERSION, name: "test-extension", display_name: "Test Extension", version: "1.0.0", From b75f8a67f9bafcfac55d3ed393b8b92b14bfd7b9 Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Sat, 1 Nov 2025 12:35:45 -0700 Subject: [PATCH 03/55] chore: update version from 1.1.5 to 1.2.0 (#147) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 77f437a..92a270e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "1.1.5", + "version": "1.2.0", "type": "module", "main": "dist/index.js", "module": "dist/index.js", From 98fc900d5ace8574db9bddec84ab2644596baa24 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 15:07:43 -0700 Subject: [PATCH 04/55] WIP --- src/schemas/any.ts | 27 ++++++++++++++++++++------- src/schemas/index.ts | 4 ++-- src/types.ts | 10 ++++++++-- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/schemas/any.ts b/src/schemas/any.ts index bd7cec8..000eba4 100644 --- a/src/schemas/any.ts +++ b/src/schemas/any.ts @@ -5,11 +5,24 @@ import * as v0_2 from "./0.2.js"; import * as v0_3 from "./0.3.js"; /** - * Union schema that accepts any supported manifest version (0.1, 0.2, 0.3). - * Use this when you need to validate manifests of any version. + * Union schema that accepts any supported manifest version. + * Uses preprocessing to normalize dxt_version to manifest_version, + * then efficiently discriminates based on the now-required manifest_version field. */ -export const McpbManifestSchema = z.union([ - v0_1.McpbManifestSchema, - v0_2.McpbManifestSchema, - v0_3.McpbManifestSchema, -]); +export const McpbManifestSchema = z.preprocess( + (val) => { + // Normalize: if it has dxt_version, ensure manifest_version is also set + if (val && typeof val === "object" && "dxt_version" in val) { + const obj = val as Record; + if (!obj.manifest_version && obj.dxt_version) { + return { ...obj, manifest_version: obj.dxt_version }; + } + } + return val; + }, + z.union([ + v0_1.McpbManifestSchema, + v0_2.McpbManifestSchema, + v0_3.McpbManifestSchema, + ]), +); diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 3729f0e..df8ff1e 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,8 +1,8 @@ export * as v0_1 from "./0.1.js"; export * as v0_2 from "./0.2.js"; export * as v0_3 from "./0.3.js"; -export * as any from "./any.js"; -export * as latest from "./latest.js"; +export * as vAny from "./any.js"; +export * as vLatest from "./latest.js"; export { MANIFEST_VERSION as LATEST_MANIFEST_VERSION, McpbManifestSchema, diff --git a/src/types.ts b/src/types.ts index 9fb510f..e6ba766 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import type * as z from "zod"; -import type { McpbManifestSchema as McpbManifestSchemaAny } from "./schemas/any.js"; +import type { McpbManifestSchema as McpbManifestSchema_vAny } from "./schemas/any.js"; import type { McpbManifestAuthorSchema, McpbManifestCompatibilitySchema, @@ -51,7 +51,7 @@ export type McpbUserConfigValues = z.infer; * McpbManifest type that accepts any supported manifest version * This is the default manifest type that should be used for maximum compatibility. */ -export type McpbManifest = z.infer; +export type McpbManifestAny = z.infer; /** * McpbManifest type for the latest manifest version only @@ -59,6 +59,12 @@ export type McpbManifest = z.infer; */ export type McpbManifestLatest = z.infer; +/** + * @deprecated Use McpbManifestAny instead + * Default manifest type that accepts any supported version + */ +export type McpbManifest = McpbManifestAny; + /** * Information about a MCPB package signature */ From f3ac79afda5d58823d2230256c0bc60912581823 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 15:27:38 -0700 Subject: [PATCH 05/55] update browser exports to incl. vAny schema --- src/browser.ts | 2 +- src/schemas/index.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/browser.ts b/src/browser.ts index d3f0598..36898b3 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,5 +1,5 @@ // Browser-compatible exports export * from "./schemas/latest.js"; +export * from "./schemas/index.js"; export * from "./shared/config.js"; -export * from "./shared/constants.js"; export * from "./types.js"; diff --git a/src/schemas/index.ts b/src/schemas/index.ts index df8ff1e..06ad45c 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,3 +1,7 @@ +import { McpbManifestSchema as ManifestSchemaV0_1 } from "./0.1.js"; +import { McpbManifestSchema as ManifestSchemaV0_2 } from "./0.2.js"; +import { McpbManifestSchema as ManifestSchemaV0_3 } from "./0.3.js"; + export * as v0_1 from "./0.1.js"; export * as v0_2 from "./0.2.js"; export * as v0_3 from "./0.3.js"; @@ -5,5 +9,14 @@ export * as vAny from "./any.js"; export * as vLatest from "./latest.js"; export { MANIFEST_VERSION as LATEST_MANIFEST_VERSION, - McpbManifestSchema, + McpbManifestSchema as LatestMcpbManifestSchema, } from "./latest.js"; + +/** + * Map of manifest versions to their strict schemas + */ +export const VERSIONED_MANIFEST_SCHEMAS = { + "0.1": ManifestSchemaV0_1, + "0.2": ManifestSchemaV0_2, + "0.3": ManifestSchemaV0_3, +} as const; \ No newline at end of file From 86166630b449a31e5933e8b6b893a80a239bc45e Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 15:37:15 -0700 Subject: [PATCH 06/55] revert: undo preprocessing changes to any.ts and types.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the preprocessing logic and type name changes to restore the simpler union schema approach. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/schemas/any.ts | 27 +++++++-------------------- src/types.ts | 10 ++-------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/src/schemas/any.ts b/src/schemas/any.ts index 000eba4..bd7cec8 100644 --- a/src/schemas/any.ts +++ b/src/schemas/any.ts @@ -5,24 +5,11 @@ import * as v0_2 from "./0.2.js"; import * as v0_3 from "./0.3.js"; /** - * Union schema that accepts any supported manifest version. - * Uses preprocessing to normalize dxt_version to manifest_version, - * then efficiently discriminates based on the now-required manifest_version field. + * Union schema that accepts any supported manifest version (0.1, 0.2, 0.3). + * Use this when you need to validate manifests of any version. */ -export const McpbManifestSchema = z.preprocess( - (val) => { - // Normalize: if it has dxt_version, ensure manifest_version is also set - if (val && typeof val === "object" && "dxt_version" in val) { - const obj = val as Record; - if (!obj.manifest_version && obj.dxt_version) { - return { ...obj, manifest_version: obj.dxt_version }; - } - } - return val; - }, - z.union([ - v0_1.McpbManifestSchema, - v0_2.McpbManifestSchema, - v0_3.McpbManifestSchema, - ]), -); +export const McpbManifestSchema = z.union([ + v0_1.McpbManifestSchema, + v0_2.McpbManifestSchema, + v0_3.McpbManifestSchema, +]); diff --git a/src/types.ts b/src/types.ts index e6ba766..9fb510f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import type * as z from "zod"; -import type { McpbManifestSchema as McpbManifestSchema_vAny } from "./schemas/any.js"; +import type { McpbManifestSchema as McpbManifestSchemaAny } from "./schemas/any.js"; import type { McpbManifestAuthorSchema, McpbManifestCompatibilitySchema, @@ -51,7 +51,7 @@ export type McpbUserConfigValues = z.infer; * McpbManifest type that accepts any supported manifest version * This is the default manifest type that should be used for maximum compatibility. */ -export type McpbManifestAny = z.infer; +export type McpbManifest = z.infer; /** * McpbManifest type for the latest manifest version only @@ -59,12 +59,6 @@ export type McpbManifestAny = z.infer; */ export type McpbManifestLatest = z.infer; -/** - * @deprecated Use McpbManifestAny instead - * Default manifest type that accepts any supported version - */ -export type McpbManifest = McpbManifestAny; - /** * Information about a MCPB package signature */ From 5fb25a253e2d384b90d35fb457ba20de4c8f6c99 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 15:46:37 -0700 Subject: [PATCH 07/55] add McpbManifestAny type --- package.json | 2 +- src/types.ts | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8749e20..77f437a 100644 --- a/package.json +++ b/package.json @@ -99,4 +99,4 @@ "@babel/parser": "7.27.3" }, "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f" -} \ No newline at end of file +} diff --git a/src/types.ts b/src/types.ts index 9fb510f..44997e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,18 +47,22 @@ export type McpbUserConfigurationOption = z.infer< export type McpbUserConfigValues = z.infer; -/** - * McpbManifest type that accepts any supported manifest version - * This is the default manifest type that should be used for maximum compatibility. - */ -export type McpbManifest = z.infer; - /** * McpbManifest type for the latest manifest version only * Use this when you specifically need the latest version. */ export type McpbManifestLatest = z.infer; +/** + * Alias for McpbManifestLatest for backwards compatibility. + */ +export type McpbManifest = McpbManifestLatest; + +/** + * McpbManifest type representing the union of all manifest versions + */ +export type McpbManifestAny = z.infer;; + /** * Information about a MCPB package signature */ From 2d76201204aed41f010b2ca9ff4779c2b3157548 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 16:34:02 -0700 Subject: [PATCH 08/55] Export versioned schemas and validation utilities in browser bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update browser.ts to export validation utilities - Export McpbManifestSchema for backwards compatibility - Update config.ts to accept McpbManifestAny for broader manifest version support - Add validate.ts with version-aware validation functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/browser.ts | 2 +- src/schemas/index.ts | 1 + src/schemas/validate.ts | 100 ++++++++++++++++++++++++++++++++++++++++ src/shared/config.ts | 5 +- 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/schemas/validate.ts diff --git a/src/browser.ts b/src/browser.ts index 36898b3..fcf4e14 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,5 +1,5 @@ // Browser-compatible exports -export * from "./schemas/latest.js"; export * from "./schemas/index.js"; +export * from "./schemas/validate.js"; export * from "./shared/config.js"; export * from "./types.js"; diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 06ad45c..3afe1b1 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -10,6 +10,7 @@ export * as vLatest from "./latest.js"; export { MANIFEST_VERSION as LATEST_MANIFEST_VERSION, McpbManifestSchema as LatestMcpbManifestSchema, + McpbManifestSchema, // backwards compatibility - exports latest schema } from "./latest.js"; /** diff --git a/src/schemas/validate.ts b/src/schemas/validate.ts new file mode 100644 index 0000000..89063f7 --- /dev/null +++ b/src/schemas/validate.ts @@ -0,0 +1,100 @@ +import type * as z from "zod"; + +import * as v0_1 from "./0.1.js"; +import * as v0_2 from "./0.2.js"; +import * as v0_3 from "./0.3.js"; + +type McpbManifestV0_1 = z.infer; +type McpbManifestV0_2 = z.infer; +type McpbManifestV0_3 = z.infer; + +/** + * Union type representing any supported manifest version. + * This is the return type of validateManifest. + */ +type ValidatedManifest = McpbManifestV0_1 | McpbManifestV0_2 | McpbManifestV0_3; + +/** + * Validates a manifest using the appropriate schema based on its manifest_version. + * + * This function automatically detects the manifest version from the data and applies + * the correct schema. This avoids TypeScript "excessively deep" errors that can occur + * when using union schemas across package boundaries. + * + * @param data - The manifest data to validate + * @returns The validated manifest with the correct type + * @throws ZodError if validation fails + * + * @example + * ```typescript + * import { validateManifest } from '@anthropic-ai/mcpb/browser'; + * + * const manifest = validateManifest(unknownData); + * // manifest is typed as McpbManifestAny (union of all versions) + * ``` + */ +export function validateManifest(data: unknown): ValidatedManifest { + // Read manifest_version or dxt_version from data + const version = (data as any)?.manifest_version || (data as any)?.dxt_version; + + // Use appropriate schema based on version + if (version === "0.1") { + return v0_1.McpbManifestSchema.parse(data); + } + + if (version === "0.2") { + return v0_2.McpbManifestSchema.parse(data); + } + + // Default to latest version (0.3) if not specified or if version is "0.3" + return v0_3.McpbManifestSchema.parse(data); +} + +/** + * Validates a manifest using the appropriate schema, returning a Result-style object. + * + * @param data - The manifest data to validate + * @returns An object with either { success: true, data } or { success: false, error } + * + * @example + * ```typescript + * import { validateManifestSafe } from '@anthropic-ai/mcpb/browser'; + * + * const result = validateManifestSafe(unknownData); + * if (result.success) { + * console.log(result.data); + * } else { + * console.error(result.error); + * } + * ``` + */ +export function validateManifestSafe(data: unknown): + | { success: true; data: ValidatedManifest } + | { success: false; error: unknown } { + try { + return { success: true, data: validateManifest(data) }; + } catch (error) { + return { success: false, error }; + } +} + +/** + * Get the appropriate schema for a given manifest version. + * Useful when you need to work with the schema directly. + * + * @param version - The manifest version (e.g., "0.1", "0.2", "0.3") + * @returns The schema for that version + * + * @example + * ```typescript + * import { getSchemaForVersion } from '@anthropic-ai/mcpb/browser'; + * + * const schema = getSchemaForVersion("0.2"); + * const result = schema.safeParse(data); + * ``` + */ +export function getSchemaForVersion(version: "0.1" | "0.2" | "0.3") { + if (version === "0.1") return v0_1.McpbManifestSchema; + if (version === "0.2") return v0_2.McpbManifestSchema; + return v0_3.McpbManifestSchema; +} diff --git a/src/shared/config.ts b/src/shared/config.ts index 20e4262..bf8391c 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -1,6 +1,7 @@ import type { Logger, McpbManifest, + McpbManifestAny, McpbUserConfigValues, McpServerConfig, } from "../types.js"; @@ -85,7 +86,7 @@ export function replaceVariables( } interface GetMcpConfigForManifestOptions { - manifest: McpbManifest; + manifest: McpbManifestAny; extensionPath: string; systemDirs: Record; userConfig: McpbUserConfigValues; @@ -179,7 +180,7 @@ export async function getMcpConfigForManifest( } interface HasRequiredConfigMissingOptions { - manifest: McpbManifest; + manifest: McpbManifestAny; userConfig?: McpbUserConfigValues; } From c137df512d41cfce5bc251d7089f1a2b8b3bd03f Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 19:44:19 -0700 Subject: [PATCH 09/55] Remove validate utilities from browser exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This removes the validateManifest utilities that were added to the browser bundle. These functions are not needed for the browser export. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/browser.ts | 1 - src/schemas/validate.ts | 100 ---------------------------------------- 2 files changed, 101 deletions(-) delete mode 100644 src/schemas/validate.ts diff --git a/src/browser.ts b/src/browser.ts index fcf4e14..71c124f 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,5 +1,4 @@ // Browser-compatible exports export * from "./schemas/index.js"; -export * from "./schemas/validate.js"; export * from "./shared/config.js"; export * from "./types.js"; diff --git a/src/schemas/validate.ts b/src/schemas/validate.ts deleted file mode 100644 index 89063f7..0000000 --- a/src/schemas/validate.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type * as z from "zod"; - -import * as v0_1 from "./0.1.js"; -import * as v0_2 from "./0.2.js"; -import * as v0_3 from "./0.3.js"; - -type McpbManifestV0_1 = z.infer; -type McpbManifestV0_2 = z.infer; -type McpbManifestV0_3 = z.infer; - -/** - * Union type representing any supported manifest version. - * This is the return type of validateManifest. - */ -type ValidatedManifest = McpbManifestV0_1 | McpbManifestV0_2 | McpbManifestV0_3; - -/** - * Validates a manifest using the appropriate schema based on its manifest_version. - * - * This function automatically detects the manifest version from the data and applies - * the correct schema. This avoids TypeScript "excessively deep" errors that can occur - * when using union schemas across package boundaries. - * - * @param data - The manifest data to validate - * @returns The validated manifest with the correct type - * @throws ZodError if validation fails - * - * @example - * ```typescript - * import { validateManifest } from '@anthropic-ai/mcpb/browser'; - * - * const manifest = validateManifest(unknownData); - * // manifest is typed as McpbManifestAny (union of all versions) - * ``` - */ -export function validateManifest(data: unknown): ValidatedManifest { - // Read manifest_version or dxt_version from data - const version = (data as any)?.manifest_version || (data as any)?.dxt_version; - - // Use appropriate schema based on version - if (version === "0.1") { - return v0_1.McpbManifestSchema.parse(data); - } - - if (version === "0.2") { - return v0_2.McpbManifestSchema.parse(data); - } - - // Default to latest version (0.3) if not specified or if version is "0.3" - return v0_3.McpbManifestSchema.parse(data); -} - -/** - * Validates a manifest using the appropriate schema, returning a Result-style object. - * - * @param data - The manifest data to validate - * @returns An object with either { success: true, data } or { success: false, error } - * - * @example - * ```typescript - * import { validateManifestSafe } from '@anthropic-ai/mcpb/browser'; - * - * const result = validateManifestSafe(unknownData); - * if (result.success) { - * console.log(result.data); - * } else { - * console.error(result.error); - * } - * ``` - */ -export function validateManifestSafe(data: unknown): - | { success: true; data: ValidatedManifest } - | { success: false; error: unknown } { - try { - return { success: true, data: validateManifest(data) }; - } catch (error) { - return { success: false, error }; - } -} - -/** - * Get the appropriate schema for a given manifest version. - * Useful when you need to work with the schema directly. - * - * @param version - The manifest version (e.g., "0.1", "0.2", "0.3") - * @returns The schema for that version - * - * @example - * ```typescript - * import { getSchemaForVersion } from '@anthropic-ai/mcpb/browser'; - * - * const schema = getSchemaForVersion("0.2"); - * const result = schema.safeParse(data); - * ``` - */ -export function getSchemaForVersion(version: "0.1" | "0.2" | "0.3") { - if (version === "0.1") return v0_1.McpbManifestSchema; - if (version === "0.2") return v0_2.McpbManifestSchema; - return v0_3.McpbManifestSchema; -} From 05187d72359bf77c631b2dc772a6ae24d2fa1023 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 20:27:58 -0700 Subject: [PATCH 10/55] Remove zod resolution and bump version to 1.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed zod version resolution from package.json - Bumped version from 1.1.5 to 1.2.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package.json | 2 +- src/schemas/index.ts | 1 + yarn.lock | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 77f437a..92a270e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "1.1.5", + "version": "1.2.0", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 3afe1b1..c87eb72 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -11,6 +11,7 @@ export { MANIFEST_VERSION as LATEST_MANIFEST_VERSION, McpbManifestSchema as LatestMcpbManifestSchema, McpbManifestSchema, // backwards compatibility - exports latest schema + McpbUserConfigValuesSchema, // lightweight schema for direct import without loading full vLatest namespace } from "./latest.js"; /** diff --git a/yarn.lock b/yarn.lock index 593a7a5..220968b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6398,8 +6398,8 @@ __metadata: linkType: hard "zod@npm:^3.25.67": - version: 3.25.67 - resolution: "zod@npm:3.25.67" - checksum: 10c0/80a0cab3033272c4ab9312198081f0c4ea88e9673c059aa36dc32024906363729db54bdb78f3dc9d5529bd1601f74974d5a56c0a23e40c6f04a9270c9ff22336 + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c languageName: node linkType: hard From 3538bd4f3b8b4d23b942bf85fe9caa72b1aa8d0a Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 20:29:04 -0700 Subject: [PATCH 11/55] Bump version to 1.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 92a270e..4e7f01b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "1.2.0", + "version": "1.3.0", "type": "module", "main": "dist/index.js", "module": "dist/index.js", From 753f27e1ba0786421166d8b542ee5341c8d5479f Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 20:31:26 -0700 Subject: [PATCH 12/55] Revert yarn.lock to match main (zod 3.25.67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 220968b..593a7a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6398,8 +6398,8 @@ __metadata: linkType: hard "zod@npm:^3.25.67": - version: 3.25.76 - resolution: "zod@npm:3.25.76" - checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c + version: 3.25.67 + resolution: "zod@npm:3.25.67" + checksum: 10c0/80a0cab3033272c4ab9312198081f0c4ea88e9673c059aa36dc32024906363729db54bdb78f3dc9d5529bd1601f74974d5a56c0a23e40c6f04a9270c9ff22336 languageName: node linkType: hard From 7f40d5be4228d860861fd9deb8e2a8ee9619a10f Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 20:34:47 -0700 Subject: [PATCH 13/55] rm McpbUserConfigValuesSchema export --- src/schemas/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/schemas/index.ts b/src/schemas/index.ts index c87eb72..3afe1b1 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -11,7 +11,6 @@ export { MANIFEST_VERSION as LATEST_MANIFEST_VERSION, McpbManifestSchema as LatestMcpbManifestSchema, McpbManifestSchema, // backwards compatibility - exports latest schema - McpbUserConfigValuesSchema, // lightweight schema for direct import without loading full vLatest namespace } from "./latest.js"; /** From e7d6ea3e6819ed32caeeefccd83f1ed69439f4f7 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 20:37:15 -0700 Subject: [PATCH 14/55] Fix type error in buildManifest return type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed return type to use the specific v0.2 manifest schema type since the function returns a manifest with DEFAULT_MANIFEST_VERSION (0.2). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/init.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cli/init.ts b/src/cli/init.ts index 7682e0f..b462ced 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -3,7 +3,8 @@ import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join, resolve } from "path"; import { DEFAULT_MANIFEST_VERSION } from "../shared/constants.js"; -import type { McpbManifest } from "../types.js"; +import type { McpbManifestSchema as McpbManifestSchemaV0_2 } from "../schemas/0.2.js"; +import type * as z from "zod"; interface PackageJson { name?: string; @@ -878,7 +879,7 @@ export function buildManifest( resources: string; default_locale: string; }, -): McpbManifest { +): z.infer { const { name, displayName, version, description, authorName } = basicInfo; const { authorEmail, authorUrl } = authorInfo; const { serverType, entryPoint, mcp_config } = serverConfig; From d7f76fc02d428c5c476af5cbd124e62664b632a0 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Sat, 1 Nov 2025 20:41:34 -0700 Subject: [PATCH 15/55] Fix linting issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused McpbManifest import from config.ts - Auto-fix formatting and import sorting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/init.ts | 6 +++--- src/schemas/index.ts | 2 +- src/shared/config.ts | 1 - src/types.ts | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/cli/init.ts b/src/cli/init.ts index b462ced..3b2ad80 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -1,11 +1,11 @@ import { confirm, input, select } from "@inquirer/prompts"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join, resolve } from "path"; - -import { DEFAULT_MANIFEST_VERSION } from "../shared/constants.js"; -import type { McpbManifestSchema as McpbManifestSchemaV0_2 } from "../schemas/0.2.js"; import type * as z from "zod"; +import type { McpbManifestSchema as McpbManifestSchemaV0_2 } from "../schemas/0.2.js"; +import { DEFAULT_MANIFEST_VERSION } from "../shared/constants.js"; + interface PackageJson { name?: string; version?: string; diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 3afe1b1..70a8e32 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -20,4 +20,4 @@ export const VERSIONED_MANIFEST_SCHEMAS = { "0.1": ManifestSchemaV0_1, "0.2": ManifestSchemaV0_2, "0.3": ManifestSchemaV0_3, -} as const; \ No newline at end of file +} as const; diff --git a/src/shared/config.ts b/src/shared/config.ts index bf8391c..9ce016c 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -1,6 +1,5 @@ import type { Logger, - McpbManifest, McpbManifestAny, McpbUserConfigValues, McpServerConfig, diff --git a/src/types.ts b/src/types.ts index 44997e4..f8b7966 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,7 +61,7 @@ export type McpbManifest = McpbManifestLatest; /** * McpbManifest type representing the union of all manifest versions */ -export type McpbManifestAny = z.infer;; +export type McpbManifestAny = z.infer; /** * Information about a MCPB package signature From 30a3b8e7798792abff5a102220e6e4cf443139d6 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 15:30:03 -0800 Subject: [PATCH 16/55] Remove vLatest concept and associated files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the concept of "vLatest" throughout the codebase to encourage use of McpbManifestAny which supports all manifest versions. Changes include: - Delete src/schemas/latest.ts and src/schemas_loose/latest.ts - Remove vLatest and latest-related exports from schema index files - Remove McpbManifestLatest type, replace McpbManifest to point to McpbManifestAny - Remove LATEST_MANIFEST_VERSION, LATEST_MANIFEST_SCHEMA constants - Update all main export files (index.ts, cli.ts, node.ts) to export from 0.3.js instead of latest.js - Update tests to use v0_3.McpbManifestSchema explicitly - Add @deprecated annotation to McpbManifest directing users to McpbManifestAny This encourages better version compatibility by using the union type that supports all manifest versions rather than assuming a single "latest" version. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli.ts | 2 +- src/index.ts | 2 +- src/node.ts | 2 +- src/schemas/index.ts | 6 ------ src/schemas/latest.ts | 1 - src/schemas_loose/index.ts | 1 - src/schemas_loose/latest.ts | 1 - src/shared/constants.ts | 20 -------------------- src/types.ts | 19 ++++++------------- test/schemas.test.ts | 24 ++++++++++++------------ 10 files changed, 21 insertions(+), 57 deletions(-) delete mode 100644 src/schemas/latest.ts delete mode 100644 src/schemas_loose/latest.ts diff --git a/src/cli.ts b/src/cli.ts index 5dc861c..809d6d4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,7 +3,7 @@ export * from "./cli/init.js"; export * from "./cli/pack.js"; // Include all shared exports -export * from "./schemas/latest.js"; +export * from "./schemas/0.3.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; export * from "./types.js"; diff --git a/src/index.ts b/src/index.ts index 62ada47..114e50d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ export * from "./cli/unpack.js"; export * from "./node/files.js"; export * from "./node/sign.js"; export * from "./node/validate.js"; -export * from "./schemas/latest.js"; +export * from "./schemas/0.3.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; export * from "./types.js"; diff --git a/src/node.ts b/src/node.ts index 12a7edb..10aa2b5 100644 --- a/src/node.ts +++ b/src/node.ts @@ -4,7 +4,7 @@ export * from "./node/sign.js"; export * from "./node/validate.js"; // Include all shared exports -export * from "./schemas/latest.js"; +export * from "./schemas/0.3.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; export * from "./types.js"; diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 70a8e32..897b95b 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -6,12 +6,6 @@ export * as v0_1 from "./0.1.js"; export * as v0_2 from "./0.2.js"; export * as v0_3 from "./0.3.js"; export * as vAny from "./any.js"; -export * as vLatest from "./latest.js"; -export { - MANIFEST_VERSION as LATEST_MANIFEST_VERSION, - McpbManifestSchema as LatestMcpbManifestSchema, - McpbManifestSchema, // backwards compatibility - exports latest schema -} from "./latest.js"; /** * Map of manifest versions to their strict schemas diff --git a/src/schemas/latest.ts b/src/schemas/latest.ts deleted file mode 100644 index e79a613..0000000 --- a/src/schemas/latest.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./0.3.js"; diff --git a/src/schemas_loose/index.ts b/src/schemas_loose/index.ts index 3548b1d..f84f84c 100644 --- a/src/schemas_loose/index.ts +++ b/src/schemas_loose/index.ts @@ -1,4 +1,3 @@ export * as v0_1 from "./0.1.js"; export * as v0_2 from "./0.2.js"; export * as v0_3 from "./0.3.js"; -export * as latest from "./latest.js"; diff --git a/src/schemas_loose/latest.ts b/src/schemas_loose/latest.ts deleted file mode 100644 index e79a613..0000000 --- a/src/schemas_loose/latest.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./0.3.js"; diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 6d16f5d..b39e27c 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,17 +1,9 @@ import { McpbManifestSchema as ManifestSchemaV0_1 } from "../schemas/0.1.js"; import { McpbManifestSchema as ManifestSchemaV0_2 } from "../schemas/0.2.js"; import { McpbManifestSchema as ManifestSchemaV0_3 } from "../schemas/0.3.js"; -import { McpbManifestSchema as CurrentManifestSchema } from "../schemas/latest.js"; import { McpbManifestSchema as LooseManifestSchemaV0_1 } from "../schemas_loose/0.1.js"; import { McpbManifestSchema as LooseManifestSchemaV0_2 } from "../schemas_loose/0.2.js"; import { McpbManifestSchema as LooseManifestSchemaV0_3 } from "../schemas_loose/0.3.js"; -import { McpbManifestSchema as CurrentLooseManifestSchema } from "../schemas_loose/latest.js"; - -/** - * Latest manifest version - the version that new manifests should use - * @deprecated - */ -export const LATEST_MANIFEST_VERSION = "0.3" as const; /** * Default manifest version for new packages @@ -35,15 +27,3 @@ export const MANIFEST_SCHEMAS_LOOSE = { "0.2": LooseManifestSchemaV0_2, "0.3": LooseManifestSchemaV0_3, } as const; - -/** - * Get the latest manifest schema based on LATEST_MANIFEST_VERSION - * @deprecated - */ -export const LATEST_MANIFEST_SCHEMA = CurrentManifestSchema; - -/** - * Get the latest loose manifest schema based on LATEST_MANIFEST_VERSION - * @deprecated - */ -export const LATEST_MANIFEST_SCHEMA_LOOSE = CurrentLooseManifestSchema; diff --git a/src/types.ts b/src/types.ts index f8b7966..6ca81ff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,14 +8,13 @@ import type { McpbManifestPlatformOverrideSchema, McpbManifestPromptSchema, McpbManifestRepositorySchema, - McpbManifestSchema, McpbManifestServerSchema, McpbManifestToolSchema, McpbSignatureInfoSchema, McpbUserConfigurationOptionSchema, McpbUserConfigValuesSchema, McpServerConfigSchema, -} from "./schemas/latest.js"; +} from "./schemas/0.3.js"; export type McpServerConfig = z.infer; @@ -47,22 +46,16 @@ export type McpbUserConfigurationOption = z.infer< export type McpbUserConfigValues = z.infer; -/** - * McpbManifest type for the latest manifest version only - * Use this when you specifically need the latest version. - */ -export type McpbManifestLatest = z.infer; - -/** - * Alias for McpbManifestLatest for backwards compatibility. - */ -export type McpbManifest = McpbManifestLatest; - /** * McpbManifest type representing the union of all manifest versions */ export type McpbManifestAny = z.infer; +/** + * @deprecated Use McpbManifestAny instead to support all manifest versions. + */ +export type McpbManifest = McpbManifestAny; + /** * Information about a MCPB package signature */ diff --git a/test/schemas.test.ts b/test/schemas.test.ts index 70f181f..6dab726 100644 --- a/test/schemas.test.ts +++ b/test/schemas.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from "fs"; import { join } from "path"; -import { McpbManifestSchema, v0_2, v0_3 } from "../src/schemas/index.js"; +import { v0_2, v0_3 } from "../src/schemas/index.js"; describe("McpbManifestSchema", () => { it("should validate a valid manifest", () => { @@ -9,7 +9,7 @@ describe("McpbManifestSchema", () => { const manifestContent = readFileSync(manifestPath, "utf-8"); const manifestData = JSON.parse(manifestContent); - const result = McpbManifestSchema.safeParse(manifestData); + const result = v0_3.McpbManifestSchema.safeParse(manifestData); expect(result.success).toBe(true); if (result.success) { @@ -23,11 +23,11 @@ describe("McpbManifestSchema", () => { const manifestContent = readFileSync(manifestPath, "utf-8"); const manifestData = JSON.parse(manifestContent); - const result = McpbManifestSchema.safeParse(manifestData); + const result = v0_3.McpbManifestSchema.safeParse(manifestData); expect(result.success).toBe(false); if (!result.success) { - const errors = result.error.issues.map((issue) => issue.path.join(".")); + const errors = result.error.issues.map((issue: any) => issue.path.join(".")); expect(errors).toContain("author.name"); expect(errors).toContain("author.email"); expect(errors).toContain("server.type"); @@ -134,7 +134,7 @@ describe("McpbManifestSchema", () => { }, }; - const result = McpbManifestSchema.safeParse(fullManifest); + const result = v0_3.McpbManifestSchema.safeParse(fullManifest); expect(result.success).toBe(true); if (result.success) { @@ -167,7 +167,7 @@ describe("McpbManifestSchema", () => { }, }; - const result = McpbManifestSchema.safeParse(manifest); + const result = v0_3.McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(true); }); }); @@ -270,10 +270,10 @@ describe("McpbManifestSchema", () => { default_locale: "en-US", }, }; - const result = McpbManifestSchema.safeParse(manifest); + const result = v0_3.McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(false); if (!result.success) { - const messages = result.error.issues.map((issue) => issue.message); + const messages = result.error.issues.map((issue: any) => issue.message); expect(messages.join(" ")).toContain("${locale}"); } }); @@ -286,7 +286,7 @@ describe("McpbManifestSchema", () => { default_locale: "en_us", }, }; - const result = McpbManifestSchema.safeParse(manifest); + const result = v0_3.McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(false); }); @@ -298,7 +298,7 @@ describe("McpbManifestSchema", () => { default_locale: "en-US", }, }; - const result = McpbManifestSchema.safeParse(manifest); + const result = v0_3.McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(true); }); }); @@ -322,7 +322,7 @@ describe("McpbManifestSchema", () => { ...base, icons: [{ src: "assets/icon.png", size: "16", theme: "light" }], }; - const result = McpbManifestSchema.safeParse(manifest); + const result = v0_3.McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(false); }); @@ -331,7 +331,7 @@ describe("McpbManifestSchema", () => { ...base, icons: [{ src: "assets/icon.png", size: "128x128" }], }; - const result = McpbManifestSchema.safeParse(manifest); + const result = v0_3.McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(true); }); }); From efe9e3345dd5d612257bab157dd0e0b935dc8722 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 15:33:31 -0800 Subject: [PATCH 17/55] Make McpbManifest point to v0.2 for backwards compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change McpbManifest type to point to v0.2 schema instead of McpbManifestAny - Add DEFAULT_MANIFEST_VERSION export to browser.ts - Update test fixtures to use manifest_version "0.2" when using McpbManifest type - Keep @deprecated annotation directing users to McpbManifestAny for multi-version support This maintains backwards compatibility for existing code using McpbManifest while encouraging new code to use McpbManifestAny for broader version support. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/browser.ts | 1 + src/types.ts | 4 +++- test/config.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/browser.ts b/src/browser.ts index 71c124f..037b024 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,4 +1,5 @@ // Browser-compatible exports export * from "./schemas/index.js"; export * from "./shared/config.js"; +export * from "./shared/constants.js"; export * from "./types.js"; diff --git a/src/types.ts b/src/types.ts index 6ca81ff..20296af 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import type * as z from "zod"; import type { McpbManifestSchema as McpbManifestSchemaAny } from "./schemas/any.js"; +import type { McpbManifestSchema as McpbManifestSchemaV0_2 } from "./schemas/0.2.js"; import type { McpbManifestAuthorSchema, McpbManifestCompatibilitySchema, @@ -52,9 +53,10 @@ export type McpbUserConfigValues = z.infer; export type McpbManifestAny = z.infer; /** + * McpbManifest type for v0.2 (default version) for backwards compatibility * @deprecated Use McpbManifestAny instead to support all manifest versions. */ -export type McpbManifest = McpbManifestAny; +export type McpbManifest = z.infer; /** * Information about a MCPB package signature diff --git a/test/config.test.ts b/test/config.test.ts index f7a0eb8..062c66e 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -90,7 +90,7 @@ describe("getMcpConfigForManifest", () => { }; const baseManifest: McpbManifest = { - manifest_version: "0.3", + manifest_version: "0.2", name: "test-extension", version: "1.0.0", description: "Test extension", @@ -305,7 +305,7 @@ describe("getMcpConfigForManifest", () => { describe("hasRequiredConfigMissing", () => { const baseManifest: McpbManifest = { - manifest_version: "0.3", + manifest_version: "0.2", name: "test-extension", version: "1.0.0", description: "Test extension", From 70c3615fc92e7c3beeba565c9a7ddc2aeeb496f5 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 15:35:15 -0800 Subject: [PATCH 18/55] Add LATEST_MANIFEST_VERSION export (hardcoded to 0.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export LATEST_MANIFEST_VERSION to indicate the maximum supported manifest version by the vAny schema. This allows clients to know what the highest version is that they can use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/shared/constants.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index b39e27c..53b08ed 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -5,6 +5,11 @@ import { McpbManifestSchema as LooseManifestSchemaV0_1 } from "../schemas_loose/ import { McpbManifestSchema as LooseManifestSchemaV0_2 } from "../schemas_loose/0.2.js"; import { McpbManifestSchema as LooseManifestSchemaV0_3 } from "../schemas_loose/0.3.js"; +/** + * Latest manifest version - indicates the maximum supported version by vAny schema + */ +export const LATEST_MANIFEST_VERSION = "0.3" as const; + /** * Default manifest version for new packages */ From 3fffc30560cee0ef99560c2f3e542a15c72d795e Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 15:47:05 -0800 Subject: [PATCH 19/55] Use McpbManifestDefault derived from DEFAULT_MANIFEST_VERSION MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Create McpbManifestDefault type derived from VERSIONED_MANIFEST_SCHEMAS[DEFAULT_MANIFEST_VERSION] - Update init.ts to use McpbManifestDefault instead of hardcoded v0.2 schema - Import schema types from version matching DEFAULT_MANIFEST_VERSION (0.2) - Update test to match commented-out localization field - Make McpbManifest (deprecated) point to McpbManifestDefault This makes the codebase more maintainable by deriving types from DEFAULT_MANIFEST_VERSION instead of hardcoding version references. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/init.ts | 7 +++---- src/types.ts | 19 ++++++++++++++----- test/init.test.ts | 4 ---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/cli/init.ts b/src/cli/init.ts index 3b2ad80..be8eaa5 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -1,10 +1,9 @@ import { confirm, input, select } from "@inquirer/prompts"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join, resolve } from "path"; -import type * as z from "zod"; -import type { McpbManifestSchema as McpbManifestSchemaV0_2 } from "../schemas/0.2.js"; import { DEFAULT_MANIFEST_VERSION } from "../shared/constants.js"; +import type { McpbManifestDefault } from "../types.js"; interface PackageJson { name?: string; @@ -879,7 +878,7 @@ export function buildManifest( resources: string; default_locale: string; }, -): z.infer { +): McpbManifestDefault { const { name, displayName, version, description, authorName } = basicInfo; const { authorEmail, authorUrl } = authorInfo; const { serverType, entryPoint, mcp_config } = serverConfig; @@ -907,7 +906,7 @@ export function buildManifest( ...(visualAssets.screenshots.length > 0 ? { screenshots: visualAssets.screenshots } : {}), - ...(localization ? { localization } : {}), + // ...(localization ? { localization } : {}), server: { type: serverType, entry_point: entryPoint, diff --git a/src/types.ts b/src/types.ts index 20296af..90c5c1d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,8 @@ import type * as z from "zod"; import type { McpbManifestSchema as McpbManifestSchemaAny } from "./schemas/any.js"; -import type { McpbManifestSchema as McpbManifestSchemaV0_2 } from "./schemas/0.2.js"; +import { VERSIONED_MANIFEST_SCHEMAS } from "./schemas/index.js"; +// Import schema types from the version matching DEFAULT_MANIFEST_VERSION import type { McpbManifestAuthorSchema, McpbManifestCompatibilitySchema, @@ -15,7 +16,8 @@ import type { McpbUserConfigurationOptionSchema, McpbUserConfigValuesSchema, McpServerConfigSchema, -} from "./schemas/0.3.js"; +} from "./schemas/0.2.js"; +import { DEFAULT_MANIFEST_VERSION } from "./shared/constants.js"; export type McpServerConfig = z.infer; @@ -53,10 +55,17 @@ export type McpbUserConfigValues = z.infer; export type McpbManifestAny = z.infer; /** - * McpbManifest type for v0.2 (default version) for backwards compatibility - * @deprecated Use McpbManifestAny instead to support all manifest versions. + * McpbManifest type for the DEFAULT_MANIFEST_VERSION + * Use this for creating new manifests with the default version. */ -export type McpbManifest = z.infer; +export type McpbManifestDefault = z.infer< + (typeof VERSIONED_MANIFEST_SCHEMAS)[typeof DEFAULT_MANIFEST_VERSION] +>; + +/** + * @deprecated Use McpbManifestAny instead to support all manifest versions, or McpbManifestDefault for the default version. + */ +export type McpbManifest = McpbManifestDefault; /** * Information about a MCPB package signature diff --git a/test/init.test.ts b/test/init.test.ts index a9b9bf4..def2e9c 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -351,10 +351,6 @@ describe("init functions", () => { }, ], screenshots: ["screen1.png", "screen2.png"], - localization: { - resources: "resources/${locale}.json", - default_locale: "en-US", - }, server: { type: "python", entry_point: "server/main.py", From 8018a9a2e097c8c8ae0625e6ad47a0c8082d258c Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 15:54:54 -0800 Subject: [PATCH 20/55] Remove latest schema reference from build script The build script was still importing from the deleted schemas/latest.js file. Updated to import McpbSignatureInfoSchema from 0.3.js and removed the mcpb-manifest-latest entry from versionedManifestSchemas. --- scripts/build-mcpb-schema.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/build-mcpb-schema.js b/scripts/build-mcpb-schema.js index a62fcdb..dce003b 100644 --- a/scripts/build-mcpb-schema.js +++ b/scripts/build-mcpb-schema.js @@ -1,10 +1,9 @@ -import { - McpbManifestSchema as McpbManifestSchemaLatest, - McpbSignatureInfoSchema, -} from "../dist/schemas/latest.js"; import { McpbManifestSchema as McpbManifestSchema_v0_1 } from "../dist/schemas/0.1.js"; import { McpbManifestSchema as McpbManifestSchema_v0_2 } from "../dist/schemas/0.2.js"; -import { McpbManifestSchema as McpbManifestSchema_v0_3 } from "../dist/schemas/0.3.js"; +import { + McpbManifestSchema as McpbManifestSchema_v0_3, + McpbSignatureInfoSchema, +} from "../dist/schemas/0.3.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import fs from "node:fs/promises"; import path from "node:path"; @@ -17,7 +16,6 @@ const versionedManifestSchemas = { "mcpb-manifest-v0.1": McpbManifestSchema_v0_1, "mcpb-manifest-v0.2": McpbManifestSchema_v0_2, "mcpb-manifest-v0.3": McpbManifestSchema_v0_3, - "mcpb-manifest-latest": McpbManifestSchemaLatest, }; // Other schemas From 21c97105842bf09d213d3bdc0bd35073095b8e25 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 15:57:20 -0800 Subject: [PATCH 21/55] Fix lint errors in types.ts and remove any casts - Use import type for VERSIONED_MANIFEST_SCHEMAS and DEFAULT_MANIFEST_VERSION - Remove unnecessary any type annotations from test file --- src/types.ts | 4 ++-- test/schemas.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index 90c5c1d..63e5e38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,8 @@ import type * as z from "zod"; import type { McpbManifestSchema as McpbManifestSchemaAny } from "./schemas/any.js"; -import { VERSIONED_MANIFEST_SCHEMAS } from "./schemas/index.js"; +import type { VERSIONED_MANIFEST_SCHEMAS } from "./schemas/index.js"; +import type { DEFAULT_MANIFEST_VERSION } from "./shared/constants.js"; // Import schema types from the version matching DEFAULT_MANIFEST_VERSION import type { McpbManifestAuthorSchema, @@ -17,7 +18,6 @@ import type { McpbUserConfigValuesSchema, McpServerConfigSchema, } from "./schemas/0.2.js"; -import { DEFAULT_MANIFEST_VERSION } from "./shared/constants.js"; export type McpServerConfig = z.infer; diff --git a/test/schemas.test.ts b/test/schemas.test.ts index 6dab726..52943e0 100644 --- a/test/schemas.test.ts +++ b/test/schemas.test.ts @@ -27,7 +27,7 @@ describe("McpbManifestSchema", () => { expect(result.success).toBe(false); if (!result.success) { - const errors = result.error.issues.map((issue: any) => issue.path.join(".")); + const errors = result.error.issues.map((issue) => issue.path.join(".")); expect(errors).toContain("author.name"); expect(errors).toContain("author.email"); expect(errors).toContain("server.type"); @@ -273,7 +273,7 @@ describe("McpbManifestSchema", () => { const result = v0_3.McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(false); if (!result.success) { - const messages = result.error.issues.map((issue: any) => issue.message); + const messages = result.error.issues.map((issue) => issue.message); expect(messages.join(" ")).toContain("${locale}"); } }); From 3a9ee6e0071149d1137eef0b76a991a7c3dd7a2d Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 15:59:40 -0800 Subject: [PATCH 22/55] Export schemas/index instead of specific version Changed all entry points (index.ts, cli.ts, node.ts) to export from schemas/index.js instead of schemas/0.3.js. This provides access to all versioned schemas (v0_1, v0_2, v0_3, vAny) and VERSIONED_MANIFEST_SCHEMAS without exposing a specific version. --- src/cli.ts | 2 +- src/index.ts | 2 +- src/node.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 809d6d4..a11144a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,7 +3,7 @@ export * from "./cli/init.js"; export * from "./cli/pack.js"; // Include all shared exports -export * from "./schemas/0.3.js"; +export * from "./schemas/index.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; export * from "./types.js"; diff --git a/src/index.ts b/src/index.ts index 114e50d..1aad967 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ export * from "./cli/unpack.js"; export * from "./node/files.js"; export * from "./node/sign.js"; export * from "./node/validate.js"; -export * from "./schemas/0.3.js"; +export * from "./schemas/index.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; export * from "./types.js"; diff --git a/src/node.ts b/src/node.ts index 10aa2b5..193df7b 100644 --- a/src/node.ts +++ b/src/node.ts @@ -4,7 +4,7 @@ export * from "./node/sign.js"; export * from "./node/validate.js"; // Include all shared exports -export * from "./schemas/0.3.js"; +export * from "./schemas/index.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; export * from "./types.js"; From 07b77432a6e9099dfcbce9c215bb1d4566ac287d Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 16:02:09 -0800 Subject: [PATCH 23/55] Fix lint errors: sort imports and rename unused parameter - Reorganize imports in types.ts to satisfy simple-import-sort - Rename unused localization parameter to _localization in init.ts --- src/cli/init.ts | 2 +- src/types.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/init.ts b/src/cli/init.ts index be8eaa5..a7fe811 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -874,7 +874,7 @@ export function buildManifest( license: string; repository?: { type: string; url: string }; }, - localization?: { + _localization?: { resources: string; default_locale: string; }, diff --git a/src/types.ts b/src/types.ts index 63e5e38..c949a0d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,5 @@ import type * as z from "zod"; -import type { McpbManifestSchema as McpbManifestSchemaAny } from "./schemas/any.js"; -import type { VERSIONED_MANIFEST_SCHEMAS } from "./schemas/index.js"; -import type { DEFAULT_MANIFEST_VERSION } from "./shared/constants.js"; // Import schema types from the version matching DEFAULT_MANIFEST_VERSION import type { McpbManifestAuthorSchema, @@ -18,6 +15,9 @@ import type { McpbUserConfigValuesSchema, McpServerConfigSchema, } from "./schemas/0.2.js"; +import type { McpbManifestSchema as McpbManifestSchemaAny } from "./schemas/any.js"; +import type { VERSIONED_MANIFEST_SCHEMAS } from "./schemas/index.js"; +import type { DEFAULT_MANIFEST_VERSION } from "./shared/constants.js"; export type McpServerConfig = z.infer; From e59797f9ce86fa61a7e1507cd6779fcd4857ceac Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 16:04:56 -0800 Subject: [PATCH 24/55] chore: major version bump --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4e7f01b..6a81a79 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "1.3.0", + "version": "2.0.0", "type": "module", "main": "dist/index.js", "module": "dist/index.js", @@ -99,4 +99,4 @@ "@babel/parser": "7.27.3" }, "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f" -} +} \ No newline at end of file From 08cb52bc86405d1c31b6933d80d8464ba5622515 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 16:07:24 -0800 Subject: [PATCH 25/55] remove localization param --- src/cli/init.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/init.ts b/src/cli/init.ts index a7fe811..f14b5d0 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -874,10 +874,10 @@ export function buildManifest( license: string; repository?: { type: string; url: string }; }, - _localization?: { - resources: string; - default_locale: string; - }, + // localization?: { + // resources: string; + // default_locale: string; + // }, ): McpbManifestDefault { const { name, displayName, version, description, authorName } = basicInfo; const { authorEmail, authorUrl } = authorInfo; From e4f1236bf268a8c2b2661880f64734d6f2b7e7f2 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 16:26:38 -0800 Subject: [PATCH 26/55] Fix TypeScript error: remove localization argument from buildManifest call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The buildManifest function signature has the localization parameter commented out (expecting 13 params), but the function call was still passing it as the 14th argument, causing a type error. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/init.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli/init.ts b/src/cli/init.ts index f14b5d0..3d2af9f 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -1026,7 +1026,6 @@ export async function initExtension( compatibility, userConfig, optionalFields, - localization, ); // Write manifest From 5f87ea58d68f1fb82b330a984ce318413a4418e0 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Mon, 3 Nov 2025 16:37:27 -0800 Subject: [PATCH 27/55] Comment out localization parameter in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The localization parameter is commented out in the buildManifest function signature, so it should also be commented out in the test calls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/init.ts | 6 +++--- test/init.test.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cli/init.ts b/src/cli/init.ts index 3d2af9f..f6a99a6 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -991,9 +991,9 @@ export async function initExtension( const visualAssets = nonInteractive ? { icon: "", icons: [], screenshots: [] } : await promptVisualAssets(); - const localization = nonInteractive - ? undefined - : await promptLocalization(); + // const localization = nonInteractive + // ? undefined + // : await promptLocalization(); const serverConfig = nonInteractive ? getDefaultServerConfig(packageData) : await promptServerConfig(packageData); diff --git a/test/init.test.ts b/test/init.test.ts index def2e9c..7acd57a 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -216,7 +216,7 @@ describe("init functions", () => { keywords: "", license: "", }, - undefined, + // undefined, // localization ); expect(manifest).toEqual({ @@ -315,10 +315,10 @@ describe("init functions", () => { license: "MIT", repository: { type: "git", url: "https://github.com/user/repo" }, }, - { - resources: "resources/${locale}.json", - default_locale: "en-US", - }, + // { // localization + // resources: "resources/${locale}.json", + // default_locale: "en-US", + // }, ); expect(manifest).toEqual({ From 5f6b15bc81beff615ac16d125110de7699e61b46 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Tue, 4 Nov 2025 10:33:16 -0800 Subject: [PATCH 28/55] remove da bad tings --- .vscode/settings.json | 3 +- scripts/build-mcpb-schema.js | 6 ++-- src/browser.ts | 1 - src/cli.ts | 1 - src/cli/init.ts | 7 ++-- src/index.ts | 1 - src/node.ts | 1 - src/node/sign.ts | 5 +-- src/schemas/0.1.ts | 14 -------- src/schemas/0.2.ts | 14 -------- src/schemas/0.3.ts | 14 -------- src/schemas_loose/0.1.ts | 14 -------- src/schemas_loose/0.2.ts | 14 -------- src/schemas_loose/0.3.ts | 14 -------- src/shared/common.ts | 15 +++++++++ src/shared/config.ts | 23 +++++++------ src/types.ts | 65 ------------------------------------ test/config.test.ts | 32 +++++++++--------- 18 files changed, 55 insertions(+), 189 deletions(-) create mode 100644 src/shared/common.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 14ac857..73dc7c2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,6 @@ "eslint.validate": ["typescript"], "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/scripts/build-mcpb-schema.js b/scripts/build-mcpb-schema.js index dce003b..6d884e1 100644 --- a/scripts/build-mcpb-schema.js +++ b/scripts/build-mcpb-schema.js @@ -1,9 +1,7 @@ import { McpbManifestSchema as McpbManifestSchema_v0_1 } from "../dist/schemas/0.1.js"; import { McpbManifestSchema as McpbManifestSchema_v0_2 } from "../dist/schemas/0.2.js"; -import { - McpbManifestSchema as McpbManifestSchema_v0_3, - McpbSignatureInfoSchema, -} from "../dist/schemas/0.3.js"; +import { McpbManifestSchema as McpbManifestSchema_v0_3 } from "../dist/schemas/0.3.js"; +import { McpbSignatureInfoSchema } from "../dist/shared/common.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import fs from "node:fs/promises"; import path from "node:path"; diff --git a/src/browser.ts b/src/browser.ts index 037b024..b8d3fa9 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -2,4 +2,3 @@ export * from "./schemas/index.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; -export * from "./types.js"; diff --git a/src/cli.ts b/src/cli.ts index a11144a..c5266d4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,7 +6,6 @@ export * from "./cli/pack.js"; export * from "./schemas/index.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; -export * from "./types.js"; // Include node exports since CLI needs them export * from "./node/files.js"; diff --git a/src/cli/init.ts b/src/cli/init.ts index f6a99a6..9e62777 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -1,9 +1,12 @@ import { confirm, input, select } from "@inquirer/prompts"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join, resolve } from "path"; +import type { z } from "zod"; +// Import the schema for DEFAULT_MANIFEST_VERSION +// TODO: Allow dynamic manifest version choice +import type { McpbManifestSchema } from "../schemas/0.2.js"; import { DEFAULT_MANIFEST_VERSION } from "../shared/constants.js"; -import type { McpbManifestDefault } from "../types.js"; interface PackageJson { name?: string; @@ -878,7 +881,7 @@ export function buildManifest( // resources: string; // default_locale: string; // }, -): McpbManifestDefault { +): z.infer { const { name, displayName, version, description, authorName } = basicInfo; const { authorEmail, authorUrl } = authorInfo; const { serverType, entryPoint, mcp_config } = serverConfig; diff --git a/src/index.ts b/src/index.ts index 1aad967..58387f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,4 +8,3 @@ export * from "./node/validate.js"; export * from "./schemas/index.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; -export * from "./types.js"; diff --git a/src/node.ts b/src/node.ts index 193df7b..f7f48a0 100644 --- a/src/node.ts +++ b/src/node.ts @@ -7,4 +7,3 @@ export * from "./node/validate.js"; export * from "./schemas/index.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; -export * from "./types.js"; diff --git a/src/node/sign.ts b/src/node/sign.ts index bb83389..e0ed5ce 100644 --- a/src/node/sign.ts +++ b/src/node/sign.ts @@ -5,8 +5,9 @@ import forge from "node-forge"; import { tmpdir } from "os"; import { join } from "path"; import { promisify } from "util"; +import type { z } from "zod"; -import type { McpbSignatureInfo } from "../types.js"; +import type { McpbSignatureInfoSchema } from "../shared/common.js"; // Signature block markers const SIGNATURE_HEADER = "MCPB_SIG_V1"; @@ -101,7 +102,7 @@ export function signMcpbFile( */ export async function verifyMcpbFile( mcpbPath: string, -): Promise { +): Promise> { try { const fileContent = readFileSync(mcpbPath); diff --git a/src/schemas/0.1.ts b/src/schemas/0.1.ts index 4d77ccc..e41d69c 100644 --- a/src/schemas/0.1.ts +++ b/src/schemas/0.1.ts @@ -71,11 +71,6 @@ export const McpbUserConfigurationOptionSchema = z.strictObject({ max: z.number().optional(), }); -export const McpbUserConfigValuesSchema = z.record( - z.string(), - z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), -); - export const McpbManifestSchema = z .strictObject({ $schema: z.string().optional(), @@ -113,12 +108,3 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); - -export const McpbSignatureInfoSchema = z.strictObject({ - status: z.enum(["signed", "unsigned", "self-signed"]), - publisher: z.string().optional(), - issuer: z.string().optional(), - valid_from: z.string().optional(), - valid_to: z.string().optional(), - fingerprint: z.string().optional(), -}); diff --git a/src/schemas/0.2.ts b/src/schemas/0.2.ts index f5d4d03..bb1de16 100644 --- a/src/schemas/0.2.ts +++ b/src/schemas/0.2.ts @@ -71,11 +71,6 @@ export const McpbUserConfigurationOptionSchema = z.strictObject({ max: z.number().optional(), }); -export const McpbUserConfigValuesSchema = z.record( - z.string(), - z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), -); - export const McpbManifestSchema = z .strictObject({ $schema: z.string().optional(), @@ -113,12 +108,3 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); - -export const McpbSignatureInfoSchema = z.strictObject({ - status: z.enum(["signed", "unsigned", "self-signed"]), - publisher: z.string().optional(), - issuer: z.string().optional(), - valid_from: z.string().optional(), - valid_to: z.string().optional(), - fingerprint: z.string().optional(), -}); diff --git a/src/schemas/0.3.ts b/src/schemas/0.3.ts index 2bdc946..2ca0206 100644 --- a/src/schemas/0.3.ts +++ b/src/schemas/0.3.ts @@ -76,11 +76,6 @@ export const McpbUserConfigurationOptionSchema = z.strictObject({ max: z.number().optional(), }); -export const McpbUserConfigValuesSchema = z.record( - z.string(), - z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), -); - export const McpbManifestLocalizationSchema = z.strictObject({ resources: z .string() @@ -147,12 +142,3 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); - -export const McpbSignatureInfoSchema = z.strictObject({ - status: z.enum(["signed", "unsigned", "self-signed"]), - publisher: z.string().optional(), - issuer: z.string().optional(), - valid_from: z.string().optional(), - valid_to: z.string().optional(), - fingerprint: z.string().optional(), -}); diff --git a/src/schemas_loose/0.1.ts b/src/schemas_loose/0.1.ts index 2ab5e56..406d6c1 100644 --- a/src/schemas_loose/0.1.ts +++ b/src/schemas_loose/0.1.ts @@ -73,11 +73,6 @@ export const McpbUserConfigurationOptionSchema = z.object({ max: z.number().optional(), }); -export const McpbUserConfigValuesSchema = z.record( - z.string(), - z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), -); - export const McpbManifestSchema = z .object({ $schema: z.string().optional(), @@ -115,12 +110,3 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); - -export const McpbSignatureInfoSchema = z.object({ - status: z.enum(["signed", "unsigned", "self-signed"]), - publisher: z.string().optional(), - issuer: z.string().optional(), - valid_from: z.string().optional(), - valid_to: z.string().optional(), - fingerprint: z.string().optional(), -}); diff --git a/src/schemas_loose/0.2.ts b/src/schemas_loose/0.2.ts index 7cdbc1a..bc177d2 100644 --- a/src/schemas_loose/0.2.ts +++ b/src/schemas_loose/0.2.ts @@ -73,11 +73,6 @@ export const McpbUserConfigurationOptionSchema = z.object({ max: z.number().optional(), }); -export const McpbUserConfigValuesSchema = z.record( - z.string(), - z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), -); - export const McpbManifestSchema = z .object({ $schema: z.string().optional(), @@ -116,12 +111,3 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); - -export const McpbSignatureInfoSchema = z.object({ - status: z.enum(["signed", "unsigned", "self-signed"]), - publisher: z.string().optional(), - issuer: z.string().optional(), - valid_from: z.string().optional(), - valid_to: z.string().optional(), - fingerprint: z.string().optional(), -}); diff --git a/src/schemas_loose/0.3.ts b/src/schemas_loose/0.3.ts index 87ac125..a96894b 100644 --- a/src/schemas_loose/0.3.ts +++ b/src/schemas_loose/0.3.ts @@ -77,11 +77,6 @@ export const McpbUserConfigurationOptionSchema = z.object({ max: z.number().optional(), }); -export const McpbUserConfigValuesSchema = z.record( - z.string(), - z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), -); - export const McpbManifestLocalizationSchema = z .object({ resources: z @@ -153,12 +148,3 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); - -export const McpbSignatureInfoSchema = z.object({ - status: z.enum(["signed", "unsigned", "self-signed"]), - publisher: z.string().optional(), - issuer: z.string().optional(), - valid_from: z.string().optional(), - valid_to: z.string().optional(), - fingerprint: z.string().optional(), -}); diff --git a/src/shared/common.ts b/src/shared/common.ts new file mode 100644 index 0000000..9449ee7 --- /dev/null +++ b/src/shared/common.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const McpbUserConfigValuesSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), +); + +export const McpbSignatureInfoSchema = z.strictObject({ + status: z.enum(["signed", "unsigned", "self-signed"]), + publisher: z.string().optional(), + issuer: z.string().optional(), + valid_from: z.string().optional(), + valid_to: z.string().optional(), + fingerprint: z.string().optional(), +}); diff --git a/src/shared/config.ts b/src/shared/config.ts index 9ce016c..b4638bc 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -1,9 +1,7 @@ -import type { - Logger, - McpbManifestAny, - McpbUserConfigValues, - McpServerConfig, -} from "../types.js"; +import type { z } from "zod"; + +import type { Logger, McpbManifestAny } from "../types.js"; +import type { McpbUserConfigValuesSchema } from "./common.js"; /** * This file contains utility functions for handling MCPB configuration, @@ -88,14 +86,14 @@ interface GetMcpConfigForManifestOptions { manifest: McpbManifestAny; extensionPath: string; systemDirs: Record; - userConfig: McpbUserConfigValues; + userConfig: z.infer; pathSeparator: string; logger?: Logger; } export async function getMcpConfigForManifest( options: GetMcpConfigForManifestOptions, -): Promise { +): Promise { const { manifest, extensionPath, @@ -109,7 +107,7 @@ export async function getMcpConfigForManifest( return undefined; } - let result: McpServerConfig = { + let result: McpbManifestAny["server"]["mcp_config"] = { ...baseConfig, }; @@ -173,14 +171,17 @@ export async function getMcpConfigForManifest( } // Replace all variables in the config - result = replaceVariables(result, variables) as McpServerConfig; + result = replaceVariables( + result, + variables, + ) as McpbManifestAny["server"]["mcp_config"]; return result; } interface HasRequiredConfigMissingOptions { manifest: McpbManifestAny; - userConfig?: McpbUserConfigValues; + userConfig?: z.infer; } function isInvalidSingleValue(value: unknown): boolean { diff --git a/src/types.ts b/src/types.ts index c949a0d..655bffa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,77 +1,12 @@ import type * as z from "zod"; -// Import schema types from the version matching DEFAULT_MANIFEST_VERSION -import type { - McpbManifestAuthorSchema, - McpbManifestCompatibilitySchema, - McpbManifestMcpConfigSchema, - McpbManifestPlatformOverrideSchema, - McpbManifestPromptSchema, - McpbManifestRepositorySchema, - McpbManifestServerSchema, - McpbManifestToolSchema, - McpbSignatureInfoSchema, - McpbUserConfigurationOptionSchema, - McpbUserConfigValuesSchema, - McpServerConfigSchema, -} from "./schemas/0.2.js"; import type { McpbManifestSchema as McpbManifestSchemaAny } from "./schemas/any.js"; -import type { VERSIONED_MANIFEST_SCHEMAS } from "./schemas/index.js"; -import type { DEFAULT_MANIFEST_VERSION } from "./shared/constants.js"; - -export type McpServerConfig = z.infer; - -export type McpbManifestAuthor = z.infer; - -export type McpbManifestRepository = z.infer< - typeof McpbManifestRepositorySchema ->; - -export type McpbManifestPlatformOverride = z.infer< - typeof McpbManifestPlatformOverrideSchema ->; - -export type McpbManifestMcpConfig = z.infer; - -export type McpbManifestServer = z.infer; - -export type McpbManifestCompatibility = z.infer< - typeof McpbManifestCompatibilitySchema ->; - -export type McpbManifestTool = z.infer; - -export type McpbManifestPrompt = z.infer; - -export type McpbUserConfigurationOption = z.infer< - typeof McpbUserConfigurationOptionSchema ->; - -export type McpbUserConfigValues = z.infer; /** * McpbManifest type representing the union of all manifest versions */ export type McpbManifestAny = z.infer; -/** - * McpbManifest type for the DEFAULT_MANIFEST_VERSION - * Use this for creating new manifests with the default version. - */ -export type McpbManifestDefault = z.infer< - (typeof VERSIONED_MANIFEST_SCHEMAS)[typeof DEFAULT_MANIFEST_VERSION] ->; - -/** - * @deprecated Use McpbManifestAny instead to support all manifest versions, or McpbManifestDefault for the default version. - */ -export type McpbManifest = McpbManifestDefault; - -/** - * Information about a MCPB package signature - */ -export type McpbSignatureInfo = z.infer; - export interface Logger { log: (...args: unknown[]) => void; error: (...args: unknown[]) => void; diff --git a/test/config.test.ts b/test/config.test.ts index 062c66e..1c34faf 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -3,7 +3,7 @@ import { hasRequiredConfigMissing, replaceVariables, } from "../src/shared/config"; -import type { Logger, McpbManifest } from "../src/types"; +import type { Logger, McpbManifestAny } from "../src/types"; describe("replaceVariables", () => { it("should replace variables in strings", () => { @@ -89,7 +89,7 @@ describe("getMcpConfigForManifest", () => { data: "/data", }; - const baseManifest: McpbManifest = { + const baseManifest: McpbManifestAny = { manifest_version: "0.2", name: "test-extension", version: "1.0.0", @@ -114,7 +114,7 @@ describe("getMcpConfigForManifest", () => { const manifest = { ...baseManifest, server: undefined, - } as unknown as McpbManifest; + } as unknown as McpbManifestAny; const result = await getMcpConfigForManifest({ manifest, @@ -129,7 +129,7 @@ describe("getMcpConfigForManifest", () => { }); it("should return undefined when required config is missing", async () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { apiKey: { @@ -171,7 +171,7 @@ describe("getMcpConfigForManifest", () => { }); it("should apply platform overrides", async () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, server: { type: "node", @@ -203,7 +203,7 @@ describe("getMcpConfigForManifest", () => { }); it("should handle user config variable replacement with defaults", async () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { port: { @@ -236,7 +236,7 @@ describe("getMcpConfigForManifest", () => { }); it("should handle user config variable replacement with user values", async () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { paths: { @@ -270,7 +270,7 @@ describe("getMcpConfigForManifest", () => { }); it("should convert boolean user config values to strings", async () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { verbose: { @@ -304,7 +304,7 @@ describe("getMcpConfigForManifest", () => { }); describe("hasRequiredConfigMissing", () => { - const baseManifest: McpbManifest = { + const baseManifest: McpbManifestAny = { manifest_version: "0.2", name: "test-extension", version: "1.0.0", @@ -329,7 +329,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return false when no config fields are required", () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { port: { @@ -349,7 +349,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return false when required config is provided", () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { apiKey: { @@ -369,7 +369,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return true when required config is undefined", () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { apiKey: { @@ -389,7 +389,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return true when required config is empty string", () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { apiKey: { @@ -409,7 +409,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return true when required config is array with invalid values", () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { paths: { @@ -430,7 +430,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return true when required config is empty array", () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { paths: { @@ -459,7 +459,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should handle multiple required config fields", () => { - const manifest: McpbManifest = { + const manifest: McpbManifestAny = { ...baseManifest, user_config: { apiKey: { From d2826644b63ae7899b708a04a5d64c843bb5d45f Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Tue, 4 Nov 2025 13:06:58 -0800 Subject: [PATCH 29/55] Restore missing type exports for mcpb package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Export McpbManifestAny and Logger types from types.js - Export McpbSignatureInfo and McpbUserConfigValues from shared/common.ts - Add exports to both main and browser entry points - Bump version to 2.0.1 This eliminates the need for manual type inference in consuming code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package.json | 2 +- src/browser.ts | 2 ++ src/index.ts | 2 ++ src/shared/common.ts | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a81a79..703a870 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "2.0.0", + "version": "2.0.1", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/browser.ts b/src/browser.ts index b8d3fa9..92c9ab8 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,4 +1,6 @@ // Browser-compatible exports export * from "./schemas/index.js"; +export * from "./shared/common.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; +export * from "./types.js"; diff --git a/src/index.ts b/src/index.ts index 58387f8..ca55908 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,5 +6,7 @@ export * from "./node/files.js"; export * from "./node/sign.js"; export * from "./node/validate.js"; export * from "./schemas/index.js"; +export * from "./shared/common.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; +export * from "./types.js"; diff --git a/src/shared/common.ts b/src/shared/common.ts index 9449ee7..9a8aeb3 100644 --- a/src/shared/common.ts +++ b/src/shared/common.ts @@ -13,3 +13,6 @@ export const McpbSignatureInfoSchema = z.strictObject({ valid_to: z.string().optional(), fingerprint: z.string().optional(), }); + +export type McpbSignatureInfo = z.infer; +export type McpbUserConfigValues = z.infer; From 29d23abfe6f390032b4200debccf51b23a840e07 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Thu, 6 Nov 2025 09:29:33 -0800 Subject: [PATCH 30/55] fix: backport privacy_policies to 0.1, remove _meta --- schemas/mcpb-manifest-v0.1.schema.json | 10 +++++----- src/schemas/0.1.ts | 2 +- src/schemas_loose/0.1.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/schemas/mcpb-manifest-v0.1.schema.json b/schemas/mcpb-manifest-v0.1.schema.json index 17e6fd8..858bf96 100644 --- a/schemas/mcpb-manifest-v0.1.schema.json +++ b/schemas/mcpb-manifest-v0.1.schema.json @@ -307,11 +307,11 @@ "additionalProperties": false } }, - "_meta": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} + "privacy_policies": { + "type": "array", + "items": { + "type": "string", + "format": "uri" } } }, diff --git a/src/schemas/0.1.ts b/src/schemas/0.1.ts index e41d69c..c4b7513 100644 --- a/src/schemas/0.1.ts +++ b/src/schemas/0.1.ts @@ -102,7 +102,7 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), - _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), + privacy_policies: z.array(z.string().url()).optional() }) .refine((data) => !!(data.dxt_version || data.manifest_version), { message: diff --git a/src/schemas_loose/0.1.ts b/src/schemas_loose/0.1.ts index 406d6c1..dc57cc3 100644 --- a/src/schemas_loose/0.1.ts +++ b/src/schemas_loose/0.1.ts @@ -104,7 +104,7 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), - _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), + privacy_policies: z.array(z.string().url()).optional(), }) .refine((data) => !!(data.dxt_version || data.manifest_version), { message: From 671d3d6f2d0e0614e6c4e68d60649e55333f86b8 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Thu, 6 Nov 2025 11:01:20 -0800 Subject: [PATCH 31/55] rm privacy_policies --- schemas/mcpb-manifest-v0.1.schema.json | 7 ------- src/schemas/0.1.ts | 1 - src/schemas_loose/0.1.ts | 1 - 3 files changed, 9 deletions(-) diff --git a/schemas/mcpb-manifest-v0.1.schema.json b/schemas/mcpb-manifest-v0.1.schema.json index 858bf96..821a00a 100644 --- a/schemas/mcpb-manifest-v0.1.schema.json +++ b/schemas/mcpb-manifest-v0.1.schema.json @@ -306,13 +306,6 @@ ], "additionalProperties": false } - }, - "privacy_policies": { - "type": "array", - "items": { - "type": "string", - "format": "uri" - } } }, "required": [ diff --git a/src/schemas/0.1.ts b/src/schemas/0.1.ts index c4b7513..9922195 100644 --- a/src/schemas/0.1.ts +++ b/src/schemas/0.1.ts @@ -102,7 +102,6 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), - privacy_policies: z.array(z.string().url()).optional() }) .refine((data) => !!(data.dxt_version || data.manifest_version), { message: diff --git a/src/schemas_loose/0.1.ts b/src/schemas_loose/0.1.ts index dc57cc3..ab7f69c 100644 --- a/src/schemas_loose/0.1.ts +++ b/src/schemas_loose/0.1.ts @@ -104,7 +104,6 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), - privacy_policies: z.array(z.string().url()).optional(), }) .refine((data) => !!(data.dxt_version || data.manifest_version), { message: From 00e4bb9d102a6afe481faf305f760bd1bd9e0348 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Thu, 6 Nov 2025 11:02:42 -0800 Subject: [PATCH 32/55] chore: update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 703a870..acfa6fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "2.0.1", + "version": "3.0.0", "type": "module", "main": "dist/index.js", "module": "dist/index.js", From 349b0ceb47cdcfe1cfd1eb2881dde6a8ae4c73df Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Thu, 6 Nov 2025 11:10:53 -0800 Subject: [PATCH 33/55] chore: update patch version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index acfa6fc..338213a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "3.0.0", + "version": "2.0.2", "type": "module", "main": "dist/index.js", "module": "dist/index.js", From d90365e6265f99c9f1ac6ffdb6cb156f39bb447a Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Wed, 12 Nov 2025 20:27:24 -0800 Subject: [PATCH 34/55] feat: add UV runtime support in manifest v0.4 Add UV as a new server type for Python extensions that use UV runtime for dependency management instead of bundling dependencies. Schema changes (v0.4): - Add "uv" to server.type enum - Make mcp_config optional (not needed for UV type) - UV extensions rely on pyproject.toml for dependencies Documentation: - Add UV runtime section to MANIFEST.md explaining benefits and usage - Update README.md to recommend UV runtime for Python - Add complete hello-world-uv example Benefits of UV runtime: - Cross-platform support (Windows/macOS/Linux, Intel/ARM) - Small bundle size (~100 KB vs 5-10 MB) - Handles compiled dependencies portably - No user Python installation required Example manifest: { "manifest_version": "0.4", "server": { "type": "uv", "entry_point": "src/server.py" } } Co-authored-by: Joan --- MANIFEST.md | 51 +++- README.md | 12 +- examples/hello-world-uv/.mcpbignore | 8 + examples/hello-world-uv/README.md | 62 ++++ examples/hello-world-uv/icon.png | Bin 0 -> 70 bytes examples/hello-world-uv/manifest.json | 23 ++ examples/hello-world-uv/pyproject.toml | 8 + examples/hello-world-uv/src/server.py | 65 +++++ schemas/mcpb-manifest-v0.4.schema.json | 376 +++++++++++++++++++++++++ scripts/build-mcpb-schema.js | 2 + src/schemas/0.4.ts | 145 ++++++++++ src/schemas/any.ts | 4 +- src/schemas/index.ts | 3 + src/schemas_loose/0.4.ts | 151 ++++++++++ src/schemas_loose/index.ts | 1 + src/shared/constants.ts | 6 +- 16 files changed, 913 insertions(+), 4 deletions(-) create mode 100644 examples/hello-world-uv/.mcpbignore create mode 100644 examples/hello-world-uv/README.md create mode 100644 examples/hello-world-uv/icon.png create mode 100644 examples/hello-world-uv/manifest.json create mode 100644 examples/hello-world-uv/pyproject.toml create mode 100644 examples/hello-world-uv/src/server.py create mode 100644 schemas/mcpb-manifest-v0.4.schema.json create mode 100644 src/schemas/0.4.ts create mode 100644 src/schemas_loose/0.4.ts diff --git a/MANIFEST.md b/MANIFEST.md index c4cdd52..1bb7f50 100644 --- a/MANIFEST.md +++ b/MANIFEST.md @@ -1,7 +1,7 @@ # MCPB Manifest.json Spec Current version: `0.3` -Last updated: 2025-10-24 +Last updated: 2025-12-02 ## Manifest Schema @@ -398,6 +398,55 @@ The `server` object defines how to run the MCP server: ### Server Types +Four server types are supported: + +- **`node`**: Node.js server with bundled dependencies +- **`python`**: Python server with bundled dependencies +- **`binary`**: Compiled executable +- **`uv`**: Python server using UV runtime (experimental, v0.4+) + +### UV Runtime (Experimental, v0.4+) + +> **Note:** UV runtime support is experimental and may change in future versions. + +The `uv` server type enables cross-platform Python extensions without bundling dependencies. Instead, dependencies are declared in `pyproject.toml` and installed by the host application using UV. + +**Benefits:** +- Cross-platform support (Windows, macOS, Linux; Intel, ARM) +- Small bundle size (~100 KB vs 5-10 MB) +- Handles compiled dependencies (pydantic, numpy, etc.) +- No user Python installation required + +**Example:** +```json +{ + "manifest_version": "0.4", + "server": { + "type": "uv", + "entry_point": "src/server.py" + } +} +``` + +**Requirements:** +- Must include `pyproject.toml` with dependencies +- Must NOT include `server/lib/` or `server/venv/` +- `mcp_config` is optional (host manages execution) + +**Package structure:** +``` +extension.mcpb +├── manifest.json # server.type = "uv" +├── pyproject.toml # Dependencies +├── .mcpbignore # Exclude .venv, server/lib +└── src/ + └── server.py +``` + +See `examples/hello-world-uv` for a complete example. + +### Node, Python, and Binary Types + 1. **Python**: `server.type = "python"` - Requires `entry_point` to Python file - All dependencies must be bundled in the MCPB diff --git a/README.md b/README.md index 608023e..f47446c 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,22 @@ bundle.mcpb (ZIP file) ### Bundling Dependencies -**Python Bundles:** +**UV Runtime (Experimental - v0.4+):** +- Use `server.type = "uv"` in manifest +- Include `pyproject.toml` with dependencies (no bundled packages needed) +- Host application manages Python and dependencies automatically +- Works cross-platform without user Python installation +- See `examples/hello-world-uv` + +**Python Bundles (Traditional):** + +- Use `server.type = "python"` in manifest - Bundle all required packages in `server/lib/` directory - OR bundle a complete virtual environment in `server/venv/` - Use tools like `pip-tools`, `poetry`, or `pipenv` to create reproducible bundles - Set `PYTHONPATH` to include bundled packages via `mcp_config.env` +- **Limitation**: Cannot portably bundle compiled dependencies (e.g., pydantic, which the MCP Python SDK requires) **Node.js Bundles:** diff --git a/examples/hello-world-uv/.mcpbignore b/examples/hello-world-uv/.mcpbignore new file mode 100644 index 0000000..eec9004 --- /dev/null +++ b/examples/hello-world-uv/.mcpbignore @@ -0,0 +1,8 @@ +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ +.mypy_cache/ +*.egg-info/ +server/lib/ +server/venv/ diff --git a/examples/hello-world-uv/README.md b/examples/hello-world-uv/README.md new file mode 100644 index 0000000..ae898df --- /dev/null +++ b/examples/hello-world-uv/README.md @@ -0,0 +1,62 @@ +# Hello World UV Runtime Example (Experimental) + +> **Note:** UV runtime support is experimental and may change in future versions. + +This example demonstrates a minimal MCP server using **UV runtime**. + +## What is UV Runtime? + +UV runtime lets Claude Desktop automatically manage Python and dependencies for your extension: +- Downloads the correct Python version for the user's platform +- Creates an isolated virtual environment +- Installs dependencies from `pyproject.toml` +- Works cross-platform (Windows, macOS, Linux) without user setup + +## Structure + +``` +hello-world-uv/ +├── manifest.json # server.type = "uv" +├── pyproject.toml # Dependencies listed here +├── .mcpbignore # Exclude build artifacts +└── src/ + └── server.py # MCP server implementation +``` + +## Key Differences from Python Runtime + +**UV Runtime** (this example): +- `server.type = "uv"` +- No bundled dependencies +- No `mcp_config` needed +- Small bundle size (~2 KB) +- Works on any platform + +**Python Runtime** (traditional): +- `server.type = "python"` +- Must bundle dependencies in `server/lib/` +- Requires `mcp_config` with PYTHONPATH +- Larger bundle size +- Only works with pure Python (no compiled deps) + +## Installing + +```bash +mcpb pack +``` + +Install the generated `.mcpb` file in Claude Desktop. + +## Testing Locally + +```bash +# Install dependencies +uv sync + +# Run server +uv run src/server.py +``` + +## Tools + +- **say_hello** - Greets a person by name diff --git a/examples/hello-world-uv/icon.png b/examples/hello-world-uv/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..08cd6f2bfd1b53ec5a4db72bed55f40907e8bdfa GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZY8JuI3K{zz}&{z5M@%E Q4U}N;boFyt=akR{0J=3.10" + } + }, + "keywords": ["example", "hello-world", "uv"], + "license": "MIT" +} diff --git a/examples/hello-world-uv/pyproject.toml b/examples/hello-world-uv/pyproject.toml new file mode 100644 index 0000000..0eca14c --- /dev/null +++ b/examples/hello-world-uv/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "hello-world-uv" +version = "1.0.0" +description = "Simple MCP server using UV runtime" +requires-python = ">=3.10" +dependencies = [ + "mcp>=1.0.0", +] diff --git a/examples/hello-world-uv/src/server.py b/examples/hello-world-uv/src/server.py new file mode 100644 index 0000000..ef871d2 --- /dev/null +++ b/examples/hello-world-uv/src/server.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Hello World MCP server using UV runtime.""" + +import asyncio +from mcp.server.models import InitializationOptions +import mcp.types as types +from mcp.server import NotificationOptions, Server +import mcp.server.stdio + + +server = Server("hello-world-uv") + + +@server.list_tools() +async def handle_list_tools() -> list[types.Tool]: + """List available tools.""" + return [ + types.Tool( + name="say_hello", + description="Say hello to someone", + inputSchema={ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name to greet", + } + }, + "required": ["name"], + }, + ) + ] + + +@server.call_tool() +async def handle_call_tool( + name: str, arguments: dict | None +) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: + """Handle tool calls.""" + if name == "say_hello": + person_name = arguments.get("name", "World") if arguments else "World" + return [types.TextContent(type="text", text=f"Hello, {person_name}!")] + + raise ValueError(f"Unknown tool: {name}") + + +async def main(): + """Run the server.""" + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="hello-world-uv", + server_version="1.0.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/schemas/mcpb-manifest-v0.4.schema.json b/schemas/mcpb-manifest-v0.4.schema.json new file mode 100644 index 0000000..69496ba --- /dev/null +++ b/schemas/mcpb-manifest-v0.4.schema.json @@ -0,0 +1,376 @@ +{ + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "dxt_version": { + "type": "string", + "const": "0.4", + "description": "@deprecated Use manifest_version instead" + }, + "manifest_version": { + "type": "string", + "const": "0.4" + }, + "name": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "repository": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "type", + "url" + ], + "additionalProperties": false + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "documentation": { + "type": "string", + "format": "uri" + }, + "support": { + "type": "string", + "format": "uri" + }, + "icon": { + "type": "string" + }, + "icons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "size": { + "type": "string", + "pattern": "^\\d+x\\d+$" + }, + "theme": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "src", + "size" + ], + "additionalProperties": false + } + }, + "screenshots": { + "type": "array", + "items": { + "type": "string" + } + }, + "localization": { + "type": "object", + "properties": { + "resources": { + "type": "string", + "pattern": "\\$\\{locale\\}" + }, + "default_locale": { + "type": "string", + "pattern": "^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$" + } + }, + "required": [ + "resources", + "default_locale" + ], + "additionalProperties": false + }, + "server": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "python", + "node", + "binary", + "uv" + ] + }, + "entry_point": { + "type": "string" + }, + "mcp_config": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "platform_overrides": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "command": { + "$ref": "#/properties/server/properties/mcp_config/properties/command" + }, + "args": { + "$ref": "#/properties/server/properties/mcp_config/properties/args" + }, + "env": { + "$ref": "#/properties/server/properties/mcp_config/properties/env" + } + }, + "additionalProperties": false + } + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "entry_point" + ], + "additionalProperties": false + }, + "tools": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "tools_generated": { + "type": "boolean" + }, + "prompts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "arguments": { + "type": "array", + "items": { + "type": "string" + } + }, + "text": { + "type": "string" + } + }, + "required": [ + "name", + "text" + ], + "additionalProperties": false + } + }, + "prompts_generated": { + "type": "boolean" + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "license": { + "type": "string" + }, + "privacy_policies": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + }, + "compatibility": { + "type": "object", + "properties": { + "claude_desktop": { + "type": "string" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "darwin", + "win32", + "linux" + ] + } + }, + "runtimes": { + "type": "object", + "properties": { + "python": { + "type": "string" + }, + "node": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "user_config": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "directory", + "file" + ] + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "multiple": { + "type": "boolean" + }, + "sensitive": { + "type": "boolean" + }, + "min": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "required": [ + "type", + "title", + "description" + ], + "additionalProperties": false + } + }, + "_meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + } + }, + "required": [ + "name", + "version", + "description", + "author", + "server" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/scripts/build-mcpb-schema.js b/scripts/build-mcpb-schema.js index 6d884e1..7f906eb 100644 --- a/scripts/build-mcpb-schema.js +++ b/scripts/build-mcpb-schema.js @@ -1,6 +1,7 @@ import { McpbManifestSchema as McpbManifestSchema_v0_1 } from "../dist/schemas/0.1.js"; import { McpbManifestSchema as McpbManifestSchema_v0_2 } from "../dist/schemas/0.2.js"; import { McpbManifestSchema as McpbManifestSchema_v0_3 } from "../dist/schemas/0.3.js"; +import { McpbManifestSchema as McpbManifestSchema_v0_4 } from "../dist/schemas/0.4.js"; import { McpbSignatureInfoSchema } from "../dist/shared/common.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import fs from "node:fs/promises"; @@ -14,6 +15,7 @@ const versionedManifestSchemas = { "mcpb-manifest-v0.1": McpbManifestSchema_v0_1, "mcpb-manifest-v0.2": McpbManifestSchema_v0_2, "mcpb-manifest-v0.3": McpbManifestSchema_v0_3, + "mcpb-manifest-v0.4": McpbManifestSchema_v0_4, }; // Other schemas diff --git a/src/schemas/0.4.ts b/src/schemas/0.4.ts new file mode 100644 index 0000000..353c787 --- /dev/null +++ b/src/schemas/0.4.ts @@ -0,0 +1,145 @@ +// v0.4: Adds UV runtime support for Python extensions +import * as z from "zod"; + +export const MANIFEST_VERSION = "0.4"; + +const LOCALE_PLACEHOLDER_REGEX = /\$\{locale\}/i; +const BCP47_REGEX = /^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$/; +const ICON_SIZE_REGEX = /^\d+x\d+$/; + +export const McpServerConfigSchema = z.strictObject({ + command: z.string(), + args: z.array(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), +}); + +export const McpbManifestAuthorSchema = z.strictObject({ + name: z.string(), + email: z.string().email().optional(), + url: z.string().url().optional(), +}); + +export const McpbManifestRepositorySchema = z.strictObject({ + type: z.string(), + url: z.string().url(), +}); + +export const McpbManifestPlatformOverrideSchema = + McpServerConfigSchema.partial(); + +export const McpbManifestMcpConfigSchema = McpServerConfigSchema.extend({ + platform_overrides: z + .record(z.string(), McpbManifestPlatformOverrideSchema) + .optional(), +}); + +export const McpbManifestServerSchema = z.strictObject({ + type: z.enum(["python", "node", "binary", "uv"]), + entry_point: z.string(), + // mcp_config is optional for UV type (UV handles execution) + mcp_config: McpbManifestMcpConfigSchema.optional(), +}); + +export const McpbManifestCompatibilitySchema = z.strictObject({ + claude_desktop: z.string().optional(), + platforms: z.array(z.enum(["darwin", "win32", "linux"])).optional(), + runtimes: z + .strictObject({ + python: z.string().optional(), + node: z.string().optional(), + }) + .optional(), +}); + +export const McpbManifestToolSchema = z.strictObject({ + name: z.string(), + description: z.string().optional(), +}); + +export const McpbManifestPromptSchema = z.strictObject({ + name: z.string(), + description: z.string().optional(), + arguments: z.array(z.string()).optional(), + text: z.string(), +}); + +export const McpbUserConfigurationOptionSchema = z.strictObject({ + type: z.enum(["string", "number", "boolean", "directory", "file"]), + title: z.string(), + description: z.string(), + required: z.boolean().optional(), + default: z + .union([z.string(), z.number(), z.boolean(), z.array(z.string())]) + .optional(), + multiple: z.boolean().optional(), + sensitive: z.boolean().optional(), + min: z.number().optional(), + max: z.number().optional(), +}); + +export const McpbManifestLocalizationSchema = z.strictObject({ + resources: z + .string() + .regex( + LOCALE_PLACEHOLDER_REGEX, + 'resources must include a "${locale}" placeholder', + ), + default_locale: z + .string() + .regex( + BCP47_REGEX, + "default_locale must be a valid BCP 47 locale identifier", + ), +}); + +export const McpbManifestIconSchema = z.strictObject({ + src: z.string(), + size: z + .string() + .regex( + ICON_SIZE_REGEX, + 'size must be in the format "WIDTHxHEIGHT" (e.g., "16x16")', + ), + theme: z.string().min(1, "theme cannot be empty when provided").optional(), +}); + +export const McpbManifestSchema = z + .strictObject({ + $schema: z.string().optional(), + dxt_version: z + .literal(MANIFEST_VERSION) + .optional() + .describe("@deprecated Use manifest_version instead"), + manifest_version: z.literal(MANIFEST_VERSION).optional(), + name: z.string(), + display_name: z.string().optional(), + version: z.string(), + description: z.string(), + long_description: z.string().optional(), + author: McpbManifestAuthorSchema, + repository: McpbManifestRepositorySchema.optional(), + homepage: z.string().url().optional(), + documentation: z.string().url().optional(), + support: z.string().url().optional(), + icon: z.string().optional(), + icons: z.array(McpbManifestIconSchema).optional(), + screenshots: z.array(z.string()).optional(), + localization: McpbManifestLocalizationSchema.optional(), + server: McpbManifestServerSchema, + tools: z.array(McpbManifestToolSchema).optional(), + tools_generated: z.boolean().optional(), + prompts: z.array(McpbManifestPromptSchema).optional(), + prompts_generated: z.boolean().optional(), + keywords: z.array(z.string()).optional(), + license: z.string().optional(), + privacy_policies: z.array(z.string().url()).optional(), + compatibility: McpbManifestCompatibilitySchema.optional(), + user_config: z + .record(z.string(), McpbUserConfigurationOptionSchema) + .optional(), + _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), + }) + .refine((data) => !!(data.dxt_version || data.manifest_version), { + message: + "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", + }); diff --git a/src/schemas/any.ts b/src/schemas/any.ts index bd7cec8..1b374c7 100644 --- a/src/schemas/any.ts +++ b/src/schemas/any.ts @@ -3,13 +3,15 @@ import * as z from "zod"; import * as v0_1 from "./0.1.js"; import * as v0_2 from "./0.2.js"; import * as v0_3 from "./0.3.js"; +import * as v0_4 from "./0.4.js"; /** - * Union schema that accepts any supported manifest version (0.1, 0.2, 0.3). + * Union schema that accepts any supported manifest version. * Use this when you need to validate manifests of any version. */ export const McpbManifestSchema = z.union([ v0_1.McpbManifestSchema, v0_2.McpbManifestSchema, v0_3.McpbManifestSchema, + v0_4.McpbManifestSchema, ]); diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 897b95b..6d1405b 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,10 +1,12 @@ import { McpbManifestSchema as ManifestSchemaV0_1 } from "./0.1.js"; import { McpbManifestSchema as ManifestSchemaV0_2 } from "./0.2.js"; import { McpbManifestSchema as ManifestSchemaV0_3 } from "./0.3.js"; +import { McpbManifestSchema as ManifestSchemaV0_4 } from "./0.4.js"; export * as v0_1 from "./0.1.js"; export * as v0_2 from "./0.2.js"; export * as v0_3 from "./0.3.js"; +export * as v0_4 from "./0.4.js"; export * as vAny from "./any.js"; /** @@ -14,4 +16,5 @@ export const VERSIONED_MANIFEST_SCHEMAS = { "0.1": ManifestSchemaV0_1, "0.2": ManifestSchemaV0_2, "0.3": ManifestSchemaV0_3, + "0.4": ManifestSchemaV0_4, } as const; diff --git a/src/schemas_loose/0.4.ts b/src/schemas_loose/0.4.ts new file mode 100644 index 0000000..35a7524 --- /dev/null +++ b/src/schemas_loose/0.4.ts @@ -0,0 +1,151 @@ +import * as z from "zod"; + +export const MANIFEST_VERSION = "0.4"; + +const LOCALE_PLACEHOLDER_REGEX = /\$\{locale\}/i; +const BCP47_REGEX = /^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$/; +const ICON_SIZE_REGEX = /^\d+x\d+$/; + +export const McpServerConfigSchema = z.object({ + command: z.string(), + args: z.array(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), +}); + +export const McpbManifestAuthorSchema = z.object({ + name: z.string(), + email: z.string().email().optional(), + url: z.string().url().optional(), +}); + +export const McpbManifestRepositorySchema = z.object({ + type: z.string(), + url: z.string().url(), +}); + +export const McpbManifestPlatformOverrideSchema = + McpServerConfigSchema.partial(); + +export const McpbManifestMcpConfigSchema = McpServerConfigSchema.extend({ + platform_overrides: z + .record(z.string(), McpbManifestPlatformOverrideSchema) + .optional(), +}); + +export const McpbManifestServerSchema = z.object({ + type: z.enum(["python", "node", "binary", "uv"]), + entry_point: z.string(), + // mcp_config is optional for UV type (UV handles execution) + mcp_config: McpbManifestMcpConfigSchema.optional(), +}); + +export const McpbManifestCompatibilitySchema = z + .object({ + claude_desktop: z.string().optional(), + platforms: z.array(z.enum(["darwin", "win32", "linux"])).optional(), + runtimes: z + .object({ + python: z.string().optional(), + node: z.string().optional(), + }) + .optional(), + }) + .passthrough(); + +export const McpbManifestToolSchema = z.object({ + name: z.string(), + description: z.string().optional(), +}); + +export const McpbManifestPromptSchema = z.object({ + name: z.string(), + description: z.string().optional(), + arguments: z.array(z.string()).optional(), + text: z.string(), +}); + +export const McpbUserConfigurationOptionSchema = z.object({ + type: z.enum(["string", "number", "boolean", "directory", "file"]), + title: z.string(), + description: z.string(), + required: z.boolean().optional(), + default: z + .union([z.string(), z.number(), z.boolean(), z.array(z.string())]) + .optional(), + multiple: z.boolean().optional(), + sensitive: z.boolean().optional(), + min: z.number().optional(), + max: z.number().optional(), +}); + +export const McpbManifestLocalizationSchema = z + .object({ + resources: z + .string() + .regex( + LOCALE_PLACEHOLDER_REGEX, + 'resources must include a "${locale}" placeholder', + ), + default_locale: z + .string() + .regex( + BCP47_REGEX, + "default_locale must be a valid BCP 47 locale identifier", + ), + }) + .passthrough(); + +export const McpbManifestIconSchema = z + .object({ + src: z.string(), + size: z + .string() + .regex( + ICON_SIZE_REGEX, + 'size must be in the format "WIDTHxHEIGHT" (e.g., "16x16")', + ), + theme: z.string().min(1).optional(), + }) + .passthrough(); + +export const McpbManifestSchema = z + .object({ + $schema: z.string().optional(), + dxt_version: z + .literal(MANIFEST_VERSION) + .optional() + .describe("@deprecated Use manifest_version instead"), + manifest_version: z.literal(MANIFEST_VERSION).optional(), + name: z.string(), + display_name: z.string().optional(), + version: z.string(), + description: z.string(), + long_description: z.string().optional(), + author: McpbManifestAuthorSchema, + repository: McpbManifestRepositorySchema.optional(), + homepage: z.string().url().optional(), + documentation: z.string().url().optional(), + support: z.string().url().optional(), + icon: z.string().optional(), + icons: z.array(McpbManifestIconSchema).optional(), + screenshots: z.array(z.string()).optional(), + localization: McpbManifestLocalizationSchema.optional(), + server: McpbManifestServerSchema, + tools: z.array(McpbManifestToolSchema).optional(), + tools_generated: z.boolean().optional(), + prompts: z.array(McpbManifestPromptSchema).optional(), + prompts_generated: z.boolean().optional(), + keywords: z.array(z.string()).optional(), + license: z.string().optional(), + privacy_policies: z.array(z.string().url()).optional(), + compatibility: McpbManifestCompatibilitySchema.optional(), + user_config: z + .record(z.string(), McpbUserConfigurationOptionSchema) + .optional(), + _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), + }) + .passthrough() + .refine((data) => !!(data.dxt_version || data.manifest_version), { + message: + "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", + }); diff --git a/src/schemas_loose/index.ts b/src/schemas_loose/index.ts index f84f84c..c2f1a01 100644 --- a/src/schemas_loose/index.ts +++ b/src/schemas_loose/index.ts @@ -1,3 +1,4 @@ export * as v0_1 from "./0.1.js"; export * as v0_2 from "./0.2.js"; export * as v0_3 from "./0.3.js"; +export * as v0_4 from "./0.4.js"; diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 53b08ed..4dd48a5 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,14 +1,16 @@ import { McpbManifestSchema as ManifestSchemaV0_1 } from "../schemas/0.1.js"; import { McpbManifestSchema as ManifestSchemaV0_2 } from "../schemas/0.2.js"; import { McpbManifestSchema as ManifestSchemaV0_3 } from "../schemas/0.3.js"; +import { McpbManifestSchema as ManifestSchemaV0_4 } from "../schemas/0.4.js"; import { McpbManifestSchema as LooseManifestSchemaV0_1 } from "../schemas_loose/0.1.js"; import { McpbManifestSchema as LooseManifestSchemaV0_2 } from "../schemas_loose/0.2.js"; import { McpbManifestSchema as LooseManifestSchemaV0_3 } from "../schemas_loose/0.3.js"; +import { McpbManifestSchema as LooseManifestSchemaV0_4 } from "../schemas_loose/0.4.js"; /** * Latest manifest version - indicates the maximum supported version by vAny schema */ -export const LATEST_MANIFEST_VERSION = "0.3" as const; +export const LATEST_MANIFEST_VERSION = "0.4" as const; /** * Default manifest version for new packages @@ -22,6 +24,7 @@ export const MANIFEST_SCHEMAS = { "0.1": ManifestSchemaV0_1, "0.2": ManifestSchemaV0_2, "0.3": ManifestSchemaV0_3, + "0.4": ManifestSchemaV0_4, } as const; /** @@ -31,4 +34,5 @@ export const MANIFEST_SCHEMAS_LOOSE = { "0.1": LooseManifestSchemaV0_1, "0.2": LooseManifestSchemaV0_2, "0.3": LooseManifestSchemaV0_3, + "0.4": LooseManifestSchemaV0_4, } as const; From ca4a8819a085bd6aec174bf448ede075750be115 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Wed, 3 Dec 2025 11:48:15 -0800 Subject: [PATCH 35/55] chore: mark v0.4 schema as experimental MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/schemas/0.4.ts | 1 + src/schemas_loose/0.4.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/schemas/0.4.ts b/src/schemas/0.4.ts index 353c787..e706e26 100644 --- a/src/schemas/0.4.ts +++ b/src/schemas/0.4.ts @@ -1,4 +1,5 @@ // v0.4: Adds UV runtime support for Python extensions +// NOTE: This schema version is experimental and subject to change. import * as z from "zod"; export const MANIFEST_VERSION = "0.4"; diff --git a/src/schemas_loose/0.4.ts b/src/schemas_loose/0.4.ts index 35a7524..d75eed3 100644 --- a/src/schemas_loose/0.4.ts +++ b/src/schemas_loose/0.4.ts @@ -1,3 +1,5 @@ +// v0.4: Adds UV runtime support for Python extensions +// NOTE: This schema version is experimental and subject to change. import * as z from "zod"; export const MANIFEST_VERSION = "0.4"; From 2ab44ffa5b1b8e8f65fd4b69869e6605bfd5a895 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Wed, 3 Dec 2025 16:51:56 -0800 Subject: [PATCH 36/55] chore: bump minor version to 2.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 338213a..f47e1f1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "2.0.2", + "version": "2.1.0", "type": "module", "main": "dist/index.js", "module": "dist/index.js", From be3b8f2561c8447f6cac7bdf23ddb23d1220147d Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Wed, 3 Dec 2025 17:04:44 -0800 Subject: [PATCH 37/55] fix: upgrade node-forge to 1.3.2 to address security vulnerabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes three known vulnerabilities: - GHSA-554w-wpv2-vw27: ASN.1 Unbounded Recursion (High) - GHSA-5gfm-wpxj-wjgq: ASN.1 Validator Desynchronization (High) - GHSA-65ch-62r8-g69g: ASN.1 OID Integer Truncation (Medium) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package.json | 6 +++--- yarn.lock | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index f47e1f1..99389da 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "2.1.0", + "version": "2.1.1", "type": "module", "main": "dist/index.js", "module": "dist/index.js", @@ -89,7 +89,7 @@ "fflate": "^0.8.2", "galactus": "^1.0.0", "ignore": "^7.0.5", - "node-forge": "^1.3.1", + "node-forge": "^1.3.2", "pretty-bytes": "^5.6.0", "zod": "^3.25.67", "zod-to-json-schema": "^3.24.6" @@ -99,4 +99,4 @@ "@babel/parser": "7.27.3" }, "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f" -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 593a7a5..538dc22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,7 +36,7 @@ __metadata: galactus: "npm:^1.0.0" ignore: "npm:^7.0.5" jest: "npm:^29.7.0" - node-forge: "npm:^1.3.1" + node-forge: "npm:^1.3.2" prettier: "npm:^3.3.3" pretty-bytes: "npm:^5.6.0" ts-jest: "npm:^29.3.2" @@ -4739,10 +4739,10 @@ __metadata: languageName: node linkType: hard -"node-forge@npm:^1.3.1": - version: 1.3.1 - resolution: "node-forge@npm:1.3.1" - checksum: 10c0/e882819b251a4321f9fc1d67c85d1501d3004b4ee889af822fd07f64de3d1a8e272ff00b689570af0465d65d6bf5074df9c76e900e0aff23e60b847f2a46fbe8 +"node-forge@npm:^1.3.2": + version: 1.3.3 + resolution: "node-forge@npm:1.3.3" + checksum: 10c0/9c6f53b0ebb34865872cf62a35b0aef8fb337e2efc766626c2e3a0040f4c02933bf29a62ba999eb44a2aca73bd512c4eda22705a47b94654b9fb8ed53db9a1db languageName: node linkType: hard From 2ed548101641deb02fe83fb2177749fccae8cd08 Mon Sep 17 00:00:00 2001 From: Alexander Sklar Date: Wed, 3 Dec 2025 11:27:59 -0800 Subject: [PATCH 38/55] Bump version and update dependencies in package.json update filesystem server version --- examples/file-system-node/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/file-system-node/package.json b/examples/file-system-node/package.json index e810564..78173f8 100644 --- a/examples/file-system-node/package.json +++ b/examples/file-system-node/package.json @@ -1,10 +1,10 @@ { "name": "ant.dir.ant.anthropic.filesystem", - "version": "0.1.3", + "version": "0.1.4", "type": "module", "private": true, "main": "server/index.js", "dependencies": { - "@modelcontextprotocol/server-filesystem": "2025.1.14" + "@modelcontextprotocol/server-filesystem": "2025.11.25" } } From 761e834ba873c2deabef106d9b66e20715b3a398 Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Wed, 3 Dec 2025 20:11:58 -0800 Subject: [PATCH 39/55] feat: add --manifest-version option to init command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow developers to specify which manifest version to use when initializing a new manifest.json file. Usage: mcpb init --manifest-version 0.3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/cli.ts | 36 +++++++++++++++++++++++------------- src/cli/init.ts | 27 ++++++++++++++++++++------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index ac38b52..7499d89 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -51,19 +51,29 @@ program .command("init [directory]") .description("Create a new MCPB extension manifest") .option("-y, --yes", "Accept all defaults (non-interactive mode)") - .action((directory?: string, options?: { yes?: boolean }) => { - void (async () => { - try { - const success = await initExtension(directory, options?.yes); - process.exit(success ? 0 : 1); - } catch (error) { - console.error( - `ERROR: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - process.exit(1); - } - })(); - }); + .option( + "--manifest-version ", + "Manifest version to use in the generated manifest", + ) + .action( + (directory?: string, options?: { yes?: boolean; manifestVersion?: string }) => { + void (async () => { + try { + const success = await initExtension( + directory, + options?.yes, + options?.manifestVersion, + ); + process.exit(success ? 0 : 1); + } catch (error) { + console.error( + `ERROR: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + process.exit(1); + } + })(); + }, + ); // Validate command program diff --git a/src/cli/init.ts b/src/cli/init.ts index 9e62777..f2dfb53 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -1,12 +1,12 @@ import { confirm, input, select } from "@inquirer/prompts"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join, resolve } from "path"; -import type { z } from "zod"; -// Import the schema for DEFAULT_MANIFEST_VERSION -// TODO: Allow dynamic manifest version choice -import type { McpbManifestSchema } from "../schemas/0.2.js"; -import { DEFAULT_MANIFEST_VERSION } from "../shared/constants.js"; +import { + DEFAULT_MANIFEST_VERSION, + MANIFEST_SCHEMAS, +} from "../shared/constants.js"; +import type { McpbManifestAny } from "../types.js"; interface PackageJson { name?: string; @@ -877,18 +877,19 @@ export function buildManifest( license: string; repository?: { type: string; url: string }; }, + manifestVersion: keyof typeof MANIFEST_SCHEMAS = DEFAULT_MANIFEST_VERSION, // localization?: { // resources: string; // default_locale: string; // }, -): z.infer { +): McpbManifestAny { const { name, displayName, version, description, authorName } = basicInfo; const { authorEmail, authorUrl } = authorInfo; const { serverType, entryPoint, mcp_config } = serverConfig; const { keywords, license, repository } = optionalFields; return { - manifest_version: DEFAULT_MANIFEST_VERSION, + manifest_version: manifestVersion, name, ...(displayName && displayName !== name ? { display_name: displayName } @@ -945,10 +946,21 @@ export function printNextSteps() { export async function initExtension( targetPath: string = process.cwd(), nonInteractive = false, + manifestVersion?: string, ): Promise { const resolvedPath = resolve(targetPath); const manifestPath = join(resolvedPath, "manifest.json"); + // Validate manifest version if provided + if (manifestVersion && !(manifestVersion in MANIFEST_SCHEMAS)) { + console.error( + `ERROR: Invalid manifest version "${manifestVersion}". Supported versions: ${Object.keys(MANIFEST_SCHEMAS).join(", ")}`, + ); + return false; + } + const effectiveManifestVersion = (manifestVersion || + DEFAULT_MANIFEST_VERSION) as keyof typeof MANIFEST_SCHEMAS; + if (existsSync(manifestPath)) { if (nonInteractive) { console.log( @@ -1029,6 +1041,7 @@ export async function initExtension( compatibility, userConfig, optionalFields, + effectiveManifestVersion, ); // Write manifest From 1dbbffc9727637a9bb4b7a796bdd45781ef9c22c Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Wed, 3 Dec 2025 20:33:10 -0800 Subject: [PATCH 40/55] fix: make mcp_config required for all server types in v0.4 schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package.json | 2 +- schemas/mcpb-manifest-v0.4.schema.json | 3 ++- src/schemas/0.4.ts | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 99389da..09a33ee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "2.1.1", + "version": "2.1.2", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/schemas/mcpb-manifest-v0.4.schema.json b/schemas/mcpb-manifest-v0.4.schema.json index 69496ba..5917415 100644 --- a/schemas/mcpb-manifest-v0.4.schema.json +++ b/schemas/mcpb-manifest-v0.4.schema.json @@ -188,7 +188,8 @@ }, "required": [ "type", - "entry_point" + "entry_point", + "mcp_config" ], "additionalProperties": false }, diff --git a/src/schemas/0.4.ts b/src/schemas/0.4.ts index e706e26..e044b91 100644 --- a/src/schemas/0.4.ts +++ b/src/schemas/0.4.ts @@ -37,8 +37,7 @@ export const McpbManifestMcpConfigSchema = McpServerConfigSchema.extend({ export const McpbManifestServerSchema = z.strictObject({ type: z.enum(["python", "node", "binary", "uv"]), entry_point: z.string(), - // mcp_config is optional for UV type (UV handles execution) - mcp_config: McpbManifestMcpConfigSchema.optional(), + mcp_config: McpbManifestMcpConfigSchema, }); export const McpbManifestCompatibilitySchema = z.strictObject({ From 163fa29a1c4c19fa308532d788006deeb021bdfb Mon Sep 17 00:00:00 2001 From: Joan Xie Date: Wed, 3 Dec 2025 21:03:04 -0800 Subject: [PATCH 41/55] style: fix Prettier formatting in cli.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Break function parameters onto separate lines with trailing comma. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/cli.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 7499d89..817b659 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -56,7 +56,10 @@ program "Manifest version to use in the generated manifest", ) .action( - (directory?: string, options?: { yes?: boolean; manifestVersion?: string }) => { + ( + directory?: string, + options?: { yes?: boolean; manifestVersion?: string }, + ) => { void (async () => { try { const success = await initExtension( From 24aa4efc88f8726bb56f6641cc21f693045f0847 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Mon, 12 Jan 2026 22:50:15 +0000 Subject: [PATCH 42/55] chore: update licensing to Apache 2.0 for new contributions --- LICENSE | 217 +++++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 2 +- 2 files changed, 214 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index 8f30a63..4a93985 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,216 @@ -Copyright 2025 Anthropic, PBC. +The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +No rights beyond those granted by the applicable original license are conveyed for such contributions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright + owner or by an individual or Legal Entity authorized to submit on behalf + of the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +--- + +MIT License + +Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Creative Commons Attribution 4.0 International (CC-BY-4.0) + +Documentation in this project (excluding specifications) is licensed under +CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for +the full license text. diff --git a/README.md b/README.md index f47446c..34aaaf5 100644 --- a/README.md +++ b/README.md @@ -169,4 +169,4 @@ yarn test # License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +This project is licensed under the Apache License 2.0 for new contributions, with existing code under MIT - see the [LICENSE](LICENSE) file for details. From 19afbcdc18ba0158cad3c70460beda2ad2786777 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 6 Feb 2026 16:13:29 -0800 Subject: [PATCH 43/55] Update MCPB signature format description Clarify the MCPB signature format in documentation. --- CLI.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLI.md b/CLI.md index 02787f8..247f211 100644 --- a/CLI.md +++ b/CLI.md @@ -292,7 +292,8 @@ MCPB uses PKCS#7 (Cryptographic Message Syntax) for digital signatures: ``` [Original MCPB ZIP content] MCPB_SIG_V1 -[Base64-encoded PKCS#7 signature] +[4-byte little-endian length prefix] +[DER-encoded PKCS#7 signature] MCPB_SIG_END ``` From bb676eb17dbfe7672badd77405c9f4c8de1b80b2 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Thu, 12 Mar 2026 08:59:13 -0500 Subject: [PATCH 44/55] fix: update ZIP EOCD comment_length when signing MCPB files (#194) The signMcpbFile function appended a signature block to the ZIP file but did not update the EOCD comment_length field. Strict ZIP parsers (like Claude Desktop's) rejected these files because the declared comment length didn't account for the appended signature data. Now scans backwards for the EOCD magic bytes, reads the current comment_length, and adds the signature block length before concatenating. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/node/sign.ts | 28 +++++++++++++++++++++++++- test/sign.e2e.test.ts | 46 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/node/sign.ts b/src/node/sign.ts index e0ed5ce..bd4c8c4 100644 --- a/src/node/sign.ts +++ b/src/node/sign.ts @@ -89,8 +89,20 @@ export function signMcpbFile( // Create signature block with PKCS#7 data const signatureBlock = createSignatureBlock(pkcs7Signature); + // Update ZIP EOCD comment_length to include signature block + // This ensures strict ZIP parsers accept the signed file + const updatedContent = Buffer.from(mcpbContent); + const eocdOffset = findEocdOffset(updatedContent); + if (eocdOffset !== -1) { + const currentCommentLength = updatedContent.readUInt16LE(eocdOffset + 20); + updatedContent.writeUInt16LE( + currentCommentLength + signatureBlock.length, + eocdOffset + 20, + ); + } + // Append signature block to MCPB file - const signedContent = Buffer.concat([mcpbContent, signatureBlock]); + const signedContent = Buffer.concat([updatedContent, signatureBlock]); writeFileSync(mcpbPath, signedContent); } @@ -218,6 +230,20 @@ export async function verifyMcpbFile( } } +/** + * Finds the offset of the ZIP End of Central Directory record + * by scanning backwards for the EOCD magic bytes (0x06054b50) + */ +function findEocdOffset(buffer: Buffer): number { + // EOCD is at least 22 bytes, scan backwards from the end + for (let i = buffer.length - 22; i >= 0; i--) { + if (buffer.readUInt32LE(i) === 0x06054b50) { + return i; + } + } + return -1; +} + /** * Creates a signature block buffer with PKCS#7 signature */ diff --git a/test/sign.e2e.test.ts b/test/sign.e2e.test.ts index cbf1c4f..9af52a0 100755 --- a/test/sign.e2e.test.ts +++ b/test/sign.e2e.test.ts @@ -468,4 +468,50 @@ describe("MCPB Signing E2E Tests", () => { it("should remove signatures", async () => { await testSignatureRemoval(); }); + + it("should update EOCD comment_length after signing", async () => { + const testFile = path.join(TEST_DIR, "test-eocd.mcpb"); + fs.copyFileSync(TEST_MCPB, testFile); + + // Read original EOCD comment_length + const originalContent = fs.readFileSync(testFile); + let eocdOffset = -1; + for (let i = originalContent.length - 22; i >= 0; i--) { + if (originalContent.readUInt32LE(i) === 0x06054b50) { + eocdOffset = i; + break; + } + } + expect(eocdOffset).toBeGreaterThanOrEqual(0); + const originalCommentLength = originalContent.readUInt16LE(eocdOffset + 20); + expect(originalCommentLength).toBe(0); // Fresh ZIP has no comment + + // Sign the file + signMcpbFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY); + + // Read signed file and verify EOCD comment_length was updated + const signedContent = fs.readFileSync(testFile); + let signedEocdOffset = -1; + for (let i = signedContent.length - 22; i >= 0; i--) { + if (signedContent.readUInt32LE(i) === 0x06054b50) { + signedEocdOffset = i; + break; + } + } + expect(signedEocdOffset).toBeGreaterThanOrEqual(0); + const signedCommentLength = signedContent.readUInt16LE( + signedEocdOffset + 20, + ); + + // Comment length should equal everything after the EOCD record's original end + const eocdMinSize = 22; // minimum EOCD size (no comment) + const dataAfterEocd = + signedContent.length - + (signedEocdOffset + eocdMinSize + originalCommentLength); + expect(signedCommentLength).toBe(dataAfterEocd); + expect(signedCommentLength).toBeGreaterThan(0); + + // Clean up + fs.unlinkSync(testFile); + }); }); From a5d9ae8ed28786a6bb7cddb114553bdf8a808dd6 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Thu, 12 Mar 2026 11:16:30 -0500 Subject: [PATCH 45/55] fix: repair broken examples and graduate UV runtime from experimental MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hello-world-node: wrap server startup in async main() with proper await on server.connect(transport) — fixes crash on initialize in Claude Desktop UtilityProcess runtime (closes #200) - hello-world-uv: add required mcp_config block — v0.4 schema requires it but the example omitted it, causing mcpb pack to fail - file-manager-python: convert from broken v0.1/python to v0.4/uv — the old manifest had privacy_policies on v0.1 (schema violation), used command "python" (ENOENT on macOS), and had no bundled deps. Now uses UV runtime with proper mcp_config (closes #47, closes #72) - Graduate UV runtime from experimental across docs and schema comments - Bump DEFAULT_MANIFEST_VERSION from 0.2 to 0.3 All three examples verified: pack succeeds, server responds to MCP initialize over stdio, tools/list returns expected tools. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 2 ++ MANIFEST.md | 6 ++-- README.md | 2 +- examples/file-manager-python/.mcpbignore | 7 +++++ examples/file-manager-python/manifest.json | 23 +++++++------- examples/file-manager-python/pyproject.toml | 31 +++---------------- examples/file-manager-python/requirements.txt | 2 -- examples/hello-world-node/server/index.js | 12 +++++-- examples/hello-world-uv/README.md | 6 ++-- examples/hello-world-uv/manifest.json | 6 +++- src/schemas/0.4.ts | 1 - src/schemas_loose/0.4.ts | 1 - src/shared/constants.ts | 2 +- 13 files changed, 44 insertions(+), 57 deletions(-) create mode 100644 examples/file-manager-python/.mcpbignore delete mode 100644 examples/file-manager-python/requirements.txt diff --git a/.gitignore b/.gitignore index ca98aa1..b01aba3 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,8 @@ typings/ # Claude files .claude.json .claude/ +CLAUDE.md +PROJECT_STATUS.md # Vite .vite/ diff --git a/MANIFEST.md b/MANIFEST.md index 1bb7f50..8ba7fa2 100644 --- a/MANIFEST.md +++ b/MANIFEST.md @@ -403,11 +403,9 @@ Four server types are supported: - **`node`**: Node.js server with bundled dependencies - **`python`**: Python server with bundled dependencies - **`binary`**: Compiled executable -- **`uv`**: Python server using UV runtime (experimental, v0.4+) +- **`uv`**: Python server using UV runtime (v0.4+) -### UV Runtime (Experimental, v0.4+) - -> **Note:** UV runtime support is experimental and may change in future versions. +### UV Runtime (v0.4+) The `uv` server type enables cross-platform Python extensions without bundling dependencies. Instead, dependencies are declared in `pyproject.toml` and installed by the host application using UV. diff --git a/README.md b/README.md index 34aaaf5..d9f1fbe 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ bundle.mcpb (ZIP file) ### Bundling Dependencies -**UV Runtime (Experimental - v0.4+):** +**UV Runtime (v0.4+):** - Use `server.type = "uv"` in manifest - Include `pyproject.toml` with dependencies (no bundled packages needed) diff --git a/examples/file-manager-python/.mcpbignore b/examples/file-manager-python/.mcpbignore new file mode 100644 index 0000000..f753254 --- /dev/null +++ b/examples/file-manager-python/.mcpbignore @@ -0,0 +1,7 @@ +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ +.mypy_cache/ +*.egg-info/ +uv.lock diff --git a/examples/file-manager-python/manifest.json b/examples/file-manager-python/manifest.json index 964b2aa..69b43e2 100644 --- a/examples/file-manager-python/manifest.json +++ b/examples/file-manager-python/manifest.json @@ -1,28 +1,28 @@ { - "$schema": "../../dist/mcpb-manifest.schema.json", - "manifest_version": "0.1", + "manifest_version": "0.4", "name": "file-manager-python", "display_name": "Python File Manager MCP", - "version": "0.1.0", + "version": "0.2.0", "description": "A Python MCP server for file operations", - "long_description": "This extension provides file management capabilities through a Python MCP server. It demonstrates Python-based MCP Bundle development, including file operations, directory management, and proper MCP protocol implementation.", + "long_description": "This extension provides file management capabilities through a Python MCP server. It demonstrates Python-based MCP Bundle development using UV runtime, including file operations, directory management, and proper MCP protocol implementation.", "author": { "name": "Anthropic", "email": "support@anthropic.com", "url": "https://github.com/anthropics" }, "server": { - "type": "python", + "type": "uv", "entry_point": "server/main.py", "mcp_config": { - "command": "python", + "command": "uv", "args": [ - "${__dirname}/server/main.py", + "run", + "--directory", "${__dirname}", + "server/main.py", "--workspace=${user_config.workspace_directory}" ], "env": { - "DEBUG": "${user_config.debug_mode}", - "PYTHONPATH": "${__dirname}/server/lib" + "DEBUG": "${user_config.debug_mode}" } } }, @@ -62,8 +62,7 @@ "claude_desktop": ">=0.10.0", "platforms": ["darwin", "win32", "linux"], "runtimes": { - "python": ">=3.8.0 <4" + "python": ">=3.10.0 <4" } - }, - "privacy_policies": [] + } } diff --git a/examples/file-manager-python/pyproject.toml b/examples/file-manager-python/pyproject.toml index 7895f32..5a1e799 100644 --- a/examples/file-manager-python/pyproject.toml +++ b/examples/file-manager-python/pyproject.toml @@ -1,31 +1,8 @@ -[build-system] -requires = ["setuptools>=42", "wheel"] -build-backend = "setuptools.build_meta" - [project] name = "file-manager-python" -version = "0.1.0" +version = "0.2.0" description = "A Python MCP server for file operations" -authors = [ - {name = "Anthropic", email = "support@anthropic.com"} +requires-python = ">=3.10" +dependencies = [ + "mcp>=1.0.0", ] -license = {text = "MIT"} -classifiers = [ - "Private :: Do Not Upload" # Prevents accidental uploads to PyPI -] - -[project.dependencies] -mcp = ">=1.0.0" -trio = ">=0.22.0" - -[tool.setuptools] -packages = ["server"] - -[project.scripts] -bundle-deps = "python:install_deps" - -# Custom scripts section for DXT development -[tool.dxt] -bundle = "pip install \"mcp[all]\" --target server/lib --upgrade --force-reinstall" -test = "python server/main.py --debug" -pack = "npx @anthropic-ai/dxt pack" \ No newline at end of file diff --git a/examples/file-manager-python/requirements.txt b/examples/file-manager-python/requirements.txt deleted file mode 100644 index d90d65c..0000000 --- a/examples/file-manager-python/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -mcp>=1.0.0 -trio>=0.22.0 diff --git a/examples/hello-world-node/server/index.js b/examples/hello-world-node/server/index.js index 03efb72..1d68293 100644 --- a/examples/hello-world-node/server/index.js +++ b/examples/hello-world-node/server/index.js @@ -64,7 +64,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }); // Start the server -const transport = new StdioServerTransport(); -server.connect(transport); +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Hello World MCP server running..."); +} -console.error("Hello World MCP server running..."); +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); diff --git a/examples/hello-world-uv/README.md b/examples/hello-world-uv/README.md index ae898df..853bb06 100644 --- a/examples/hello-world-uv/README.md +++ b/examples/hello-world-uv/README.md @@ -1,6 +1,4 @@ -# Hello World UV Runtime Example (Experimental) - -> **Note:** UV runtime support is experimental and may change in future versions. +# Hello World UV Runtime Example This example demonstrates a minimal MCP server using **UV runtime**. @@ -28,7 +26,7 @@ hello-world-uv/ **UV Runtime** (this example): - `server.type = "uv"` - No bundled dependencies -- No `mcp_config` needed +- `mcp_config` uses `uv run` to auto-resolve deps from `pyproject.toml` - Small bundle size (~2 KB) - Works on any platform diff --git a/examples/hello-world-uv/manifest.json b/examples/hello-world-uv/manifest.json index f3d3b4a..534ef4d 100644 --- a/examples/hello-world-uv/manifest.json +++ b/examples/hello-world-uv/manifest.json @@ -10,7 +10,11 @@ "icon": "icon.png", "server": { "type": "uv", - "entry_point": "src/server.py" + "entry_point": "src/server.py", + "mcp_config": { + "command": "uv", + "args": ["run", "--directory", "${__dirname}", "src/server.py"] + } }, "compatibility": { "platforms": ["darwin", "linux", "win32"], diff --git a/src/schemas/0.4.ts b/src/schemas/0.4.ts index e044b91..ae97d17 100644 --- a/src/schemas/0.4.ts +++ b/src/schemas/0.4.ts @@ -1,5 +1,4 @@ // v0.4: Adds UV runtime support for Python extensions -// NOTE: This schema version is experimental and subject to change. import * as z from "zod"; export const MANIFEST_VERSION = "0.4"; diff --git a/src/schemas_loose/0.4.ts b/src/schemas_loose/0.4.ts index d75eed3..395fd60 100644 --- a/src/schemas_loose/0.4.ts +++ b/src/schemas_loose/0.4.ts @@ -1,5 +1,4 @@ // v0.4: Adds UV runtime support for Python extensions -// NOTE: This schema version is experimental and subject to change. import * as z from "zod"; export const MANIFEST_VERSION = "0.4"; diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 4dd48a5..a7c460a 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -15,7 +15,7 @@ export const LATEST_MANIFEST_VERSION = "0.4" as const; /** * Default manifest version for new packages */ -export const DEFAULT_MANIFEST_VERSION = "0.2" as const; +export const DEFAULT_MANIFEST_VERSION = "0.3" as const; /** * Map of manifest versions to their strict schemas From cc4aa751b6eab80dcda3f29e2028c1c07c609ada Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Fri, 13 Mar 2026 16:36:47 -0500 Subject: [PATCH 46/55] feat: add Rust binary calculator example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First example of the binary server type — a compiled Rust MCP server packaged as an MCPB. Based on the official Rust SDK calculator example (rmcp 1.2.0, 2 tools: sum/sub, ~80 LOC). Includes Cargo.toml, Dockerfile for Docker-based builds, .mcpbignore to exclude source from the bundle, and README with build/pack/test instructions. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/README.md | 11 ++-- examples/calculator-rust/.gitignore | 3 + examples/calculator-rust/.mcpbignore | 7 +++ examples/calculator-rust/Cargo.toml | 18 ++++++ examples/calculator-rust/Dockerfile | 11 ++++ examples/calculator-rust/README.md | 87 ++++++++++++++++++++++++++ examples/calculator-rust/manifest.json | 36 +++++++++++ examples/calculator-rust/src/main.rs | 80 +++++++++++++++++++++++ 8 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 examples/calculator-rust/.gitignore create mode 100644 examples/calculator-rust/.mcpbignore create mode 100644 examples/calculator-rust/Cargo.toml create mode 100644 examples/calculator-rust/Dockerfile create mode 100644 examples/calculator-rust/README.md create mode 100644 examples/calculator-rust/manifest.json create mode 100644 examples/calculator-rust/src/main.rs diff --git a/examples/README.md b/examples/README.md index 24480ed..334e531 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,11 +14,12 @@ But, the MCP servers themselves are not robust secure production ready servers a ## Examples Included -| Example | Type | Demonstrates | -| --------------------- | ------- | ---------------------------------------- | -| `hello-world-node` | Node.js | Basic MCP server with simple time tool | -| `chrome-applescript` | Node.js | Browser automation via AppleScript | -| `file-manager-python` | Python | File system operations and path handling | +| Example | Type | Demonstrates | +| --------------------- | ------- | ------------------------------------------- | +| `hello-world-node` | Node.js | Basic MCP server with simple time tool | +| `chrome-applescript` | Node.js | Browser automation via AppleScript | +| `file-manager-python` | Python | File system operations and path handling | +| `calculator-rust` | Binary | Compiled Rust binary as MCP Bundle | ## Usage diff --git a/examples/calculator-rust/.gitignore b/examples/calculator-rust/.gitignore new file mode 100644 index 0000000..ae1d722 --- /dev/null +++ b/examples/calculator-rust/.gitignore @@ -0,0 +1,3 @@ +target/ +Cargo.lock +server/mcp-calculator diff --git a/examples/calculator-rust/.mcpbignore b/examples/calculator-rust/.mcpbignore new file mode 100644 index 0000000..3b44c2b --- /dev/null +++ b/examples/calculator-rust/.mcpbignore @@ -0,0 +1,7 @@ +Cargo.toml +Cargo.lock +src/ +target/ +Dockerfile +README.md +.gitignore diff --git a/examples/calculator-rust/Cargo.toml b/examples/calculator-rust/Cargo.toml new file mode 100644 index 0000000..dfbca7e --- /dev/null +++ b/examples/calculator-rust/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "mcp-calculator" +version = "1.0.0" +edition = "2024" + +[dependencies] +rmcp = { version = "1.2.0", features = ["server", "macros", "transport-io", "schemars"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] } +schemars = "1.0" + +[profile.release] +strip = true +lto = true diff --git a/examples/calculator-rust/Dockerfile b/examples/calculator-rust/Dockerfile new file mode 100644 index 0000000..a8358f5 --- /dev/null +++ b/examples/calculator-rust/Dockerfile @@ -0,0 +1,11 @@ +FROM rust:1.94-bookworm AS builder + +WORKDIR /app +COPY Cargo.toml ./ +COPY src/ ./src/ + +RUN cargo build --release + +FROM debian:bookworm-slim +COPY --from=builder /app/target/release/mcp-calculator /mcp-calculator +ENTRYPOINT ["/mcp-calculator"] diff --git a/examples/calculator-rust/README.md b/examples/calculator-rust/README.md new file mode 100644 index 0000000..adaf4d1 --- /dev/null +++ b/examples/calculator-rust/README.md @@ -0,0 +1,87 @@ +# Calculator Rust Binary Example + +Demonstrates packaging a compiled Rust binary as an MCP Bundle using the `binary` server type. Based on the [official Rust MCP SDK calculator example](https://github.com/modelcontextprotocol/rust-sdk/tree/main/examples/servers). + +## What is Binary Server Type? + +Unlike `node` or `uv` server types where the source code is bundled and executed by a runtime, the `binary` server type packages a pre-compiled native executable. This is useful for: + +- Rust, Go, C/C++ MCP servers +- Performance-sensitive workloads +- Servers with no runtime dependencies + +The tradeoff is that binaries are platform-specific — you need a separate build for each target OS/architecture. + +## Structure + +``` +calculator-rust/ +├── manifest.json # server.type = "binary" +├── Cargo.toml # Rust project +├── Dockerfile # Build via Docker +├── .mcpbignore # Exclude source from bundle +├── .gitignore # Exclude build artifacts +└── src/ + └── main.rs # Calculator MCP server (~80 LOC) +``` + +After building, the `server/` directory is created with the compiled binary. + +## Building + +### Native (requires Rust 1.85+) + +```bash +cd examples/calculator-rust +cargo build --release +mkdir -p server +cp target/release/mcp-calculator server/ +``` + +### Docker (no local Rust required) + +```bash +cd examples/calculator-rust +docker build -t mcp-calculator . +# Extract the binary (linux only) +docker create --name calc mcp-calculator +docker cp calc:/mcp-calculator server/mcp-calculator +docker rm calc +chmod +x server/mcp-calculator +``` + +## Packing + +```bash +mcpb pack examples/calculator-rust +``` + +The `.mcpbignore` file excludes source code and build artifacts — only `manifest.json` and `server/mcp-calculator` end up in the bundle. + +## Testing + +```bash +# MCP protocol handshake +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \ + | ./server/mcp-calculator + +# Tool call +(printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}\n' +printf '{"jsonrpc":"2.0","method":"notifications/initialized"}\n' +printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"sum","arguments":{"a":3,"b":4}}}\n' +sleep 1) | ./server/mcp-calculator +``` + +Expected: `sum(3, 4)` returns `{"text":"7"}`. + +## Tools + +- **sum** — Calculate the sum of two numbers +- **sub** — Calculate the difference of two numbers + +## Notes + +- Binary size: ~2.5 MB (stripped, LTO enabled) +- The `Cargo.toml` uses `edition = "2024"` (Rust 1.85+) +- Logs are written to stderr via `tracing`, so they don't interfere with MCP's stdio transport +- Uses the [rmcp](https://crates.io/crates/rmcp) crate (official Rust MCP SDK) diff --git a/examples/calculator-rust/manifest.json b/examples/calculator-rust/manifest.json new file mode 100644 index 0000000..fd31518 --- /dev/null +++ b/examples/calculator-rust/manifest.json @@ -0,0 +1,36 @@ +{ + "$schema": "../../dist/mcpb-manifest.schema.json", + "manifest_version": "0.3", + "name": "calculator-rust", + "display_name": "Calculator (Rust Binary)", + "version": "1.0.0", + "description": "A simple calculator MCP server compiled from Rust", + "long_description": "Demonstrates packaging a compiled Rust binary as an MCP Bundle using the binary server type. The server provides sum and sub tools for basic arithmetic. Based on the official Rust MCP SDK calculator example.", + "author": { + "name": "Model Context Protocol" + }, + "server": { + "type": "binary", + "entry_point": "server/mcp-calculator", + "mcp_config": { + "command": "${__dirname}/server/mcp-calculator", + "args": [], + "env": {} + } + }, + "tools": [ + { + "name": "sum", + "description": "Calculate the sum of two numbers" + }, + { + "name": "sub", + "description": "Calculate the difference of two numbers" + } + ], + "keywords": ["calculator", "rust", "binary", "example"], + "license": "Apache-2.0", + "compatibility": { + "platforms": ["darwin", "linux"] + } +} diff --git a/examples/calculator-rust/src/main.rs b/examples/calculator-rust/src/main.rs new file mode 100644 index 0000000..4a0f885 --- /dev/null +++ b/examples/calculator-rust/src/main.rs @@ -0,0 +1,80 @@ +use anyhow::Result; +use rmcp::{ + ServerHandler, ServiceExt, + handler::server::{router::tool::ToolRouter, wrapper::Parameters}, + model::{ServerCapabilities, ServerInfo}, + schemars, tool, tool_handler, tool_router, + transport::stdio, +}; +use tracing_subscriber::{self, EnvFilter}; + +// --- Calculator types --- + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct SumRequest { + #[schemars(description = "the left hand side number")] + pub a: i32, + pub b: i32, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct SubRequest { + #[schemars(description = "the left hand side number")] + pub a: i32, + #[schemars(description = "the right hand side number")] + pub b: i32, +} + +// --- Calculator server --- + +#[derive(Debug, Clone)] +pub struct Calculator { + tool_router: ToolRouter, +} + +#[tool_router] +impl Calculator { + pub fn new() -> Self { + Self { + tool_router: Self::tool_router(), + } + } + + #[tool(description = "Calculate the sum of two numbers")] + fn sum(&self, Parameters(SumRequest { a, b }): Parameters) -> String { + (a + b).to_string() + } + + #[tool(description = "Calculate the difference of two numbers")] + fn sub(&self, Parameters(SubRequest { a, b }): Parameters) -> String { + (a - b).to_string() + } +} + +#[tool_handler] +impl ServerHandler for Calculator { + fn get_info(&self) -> ServerInfo { + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + .with_instructions("A simple calculator".to_string()) + } +} + +// --- Main --- + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into())) + .with_writer(std::io::stderr) + .with_ansi(false) + .init(); + + tracing::info!("Starting Calculator MCP server"); + + let service = Calculator::new().serve(stdio()).await.inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; + + service.waiting().await?; + Ok(()) +} From a907aead99c57b32a939e5d359ffd797fad0c991 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Fri, 13 Mar 2026 16:41:30 -0500 Subject: [PATCH 47/55] chore: remove Dockerfile and .gitignore from calculator-rust example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align with other examples — no example ships a Dockerfile or its own .gitignore. Rust build artifacts are now covered by the repo-level .gitignore instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 6 ++++++ examples/calculator-rust/.gitignore | 3 --- examples/calculator-rust/.mcpbignore | 2 -- examples/calculator-rust/Dockerfile | 11 ----------- examples/calculator-rust/README.md | 16 +--------------- 5 files changed, 7 insertions(+), 31 deletions(-) delete mode 100644 examples/calculator-rust/.gitignore delete mode 100644 examples/calculator-rust/Dockerfile diff --git a/.gitignore b/.gitignore index ca98aa1..ec20709 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,8 @@ typings/ # Claude files .claude.json .claude/ +CLAUDE.md +PROJECT_STATUS.md # Vite .vite/ @@ -107,4 +109,8 @@ self-signed-key.pem invalid-json.json **/server/lib/** +# Rust example build artifacts +examples/calculator-rust/target/ +examples/calculator-rust/server/ + .yarn/install-state.gz \ No newline at end of file diff --git a/examples/calculator-rust/.gitignore b/examples/calculator-rust/.gitignore deleted file mode 100644 index ae1d722..0000000 --- a/examples/calculator-rust/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -target/ -Cargo.lock -server/mcp-calculator diff --git a/examples/calculator-rust/.mcpbignore b/examples/calculator-rust/.mcpbignore index 3b44c2b..29c73c3 100644 --- a/examples/calculator-rust/.mcpbignore +++ b/examples/calculator-rust/.mcpbignore @@ -2,6 +2,4 @@ Cargo.toml Cargo.lock src/ target/ -Dockerfile README.md -.gitignore diff --git a/examples/calculator-rust/Dockerfile b/examples/calculator-rust/Dockerfile deleted file mode 100644 index a8358f5..0000000 --- a/examples/calculator-rust/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM rust:1.94-bookworm AS builder - -WORKDIR /app -COPY Cargo.toml ./ -COPY src/ ./src/ - -RUN cargo build --release - -FROM debian:bookworm-slim -COPY --from=builder /app/target/release/mcp-calculator /mcp-calculator -ENTRYPOINT ["/mcp-calculator"] diff --git a/examples/calculator-rust/README.md b/examples/calculator-rust/README.md index adaf4d1..1482131 100644 --- a/examples/calculator-rust/README.md +++ b/examples/calculator-rust/README.md @@ -18,9 +18,7 @@ The tradeoff is that binaries are platform-specific — you need a separate buil calculator-rust/ ├── manifest.json # server.type = "binary" ├── Cargo.toml # Rust project -├── Dockerfile # Build via Docker ├── .mcpbignore # Exclude source from bundle -├── .gitignore # Exclude build artifacts └── src/ └── main.rs # Calculator MCP server (~80 LOC) ``` @@ -29,7 +27,7 @@ After building, the `server/` directory is created with the compiled binary. ## Building -### Native (requires Rust 1.85+) +Requires [Rust](https://rustup.rs/) 1.85+. ```bash cd examples/calculator-rust @@ -38,18 +36,6 @@ mkdir -p server cp target/release/mcp-calculator server/ ``` -### Docker (no local Rust required) - -```bash -cd examples/calculator-rust -docker build -t mcp-calculator . -# Extract the binary (linux only) -docker create --name calc mcp-calculator -docker cp calc:/mcp-calculator server/mcp-calculator -docker rm calc -chmod +x server/mcp-calculator -``` - ## Packing ```bash From f4671638a2db51abb19dc312b00277bd58bc0b66 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Sun, 15 Mar 2026 06:54:22 -0500 Subject: [PATCH 48/55] feat: add --manifest flag to pack command (#87) Allow specifying an external manifest file path with `mcpb pack --manifest `. The manifest resolves relative to CWD, is validated, injected into the bundle, and errors immediately if the specified file is not found. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli/cli.ts | 43 ++++++++++++++++---------- src/cli/pack.ts | 24 +++++++++++++-- test/cli.test.ts | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 18 deletions(-) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 817b659..5ba121c 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -101,22 +101,33 @@ program program .command("pack [directory] [output]") .description("Pack a directory into an MCPB extension") - .action((directory: string = process.cwd(), output?: string) => { - void (async () => { - try { - const success = await packExtension({ - extensionPath: directory, - outputPath: output, - }); - process.exit(success ? 0 : 1); - } catch (error) { - console.error( - `ERROR: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - process.exit(1); - } - })(); - }); + .option( + "-m, --manifest ", + "Path to manifest file (defaults to manifest.json in directory)", + ) + .action( + ( + directory: string = process.cwd(), + output: string | undefined, + options: { manifest?: string }, + ) => { + void (async () => { + try { + const success = await packExtension({ + extensionPath: directory, + outputPath: output, + manifestPath: options.manifest, + }); + process.exit(success ? 0 : 1); + } catch (error) { + console.error( + `ERROR: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + process.exit(1); + } + })(); + }, + ); // Unpack command program diff --git a/src/cli/pack.ts b/src/cli/pack.ts index e2411bf..1e35d47 100644 --- a/src/cli/pack.ts +++ b/src/cli/pack.ts @@ -22,6 +22,7 @@ interface PackOptions { extensionPath: string; outputPath?: string; silent?: boolean; + manifestPath?: string; } function formatFileSize(bytes: number): string { @@ -50,6 +51,7 @@ export async function packExtension({ extensionPath, outputPath, silent, + manifestPath: customManifestPath, }: PackOptions): Promise { const resolvedPath = resolve(extensionPath); const logger = getLogger({ silent }); @@ -60,9 +62,18 @@ export async function packExtension({ return false; } - // Check if manifest exists - const manifestPath = join(resolvedPath, "manifest.json"); + // Resolve manifest path + const manifestPath = customManifestPath + ? resolve(customManifestPath) + : join(resolvedPath, "manifest.json"); + if (!existsSync(manifestPath)) { + if (customManifestPath) { + // When --manifest is explicitly provided, error immediately + logger.error(`ERROR: Manifest file not found: ${customManifestPath}`); + return false; + } + logger.log(`No manifest.json found in ${extensionPath}`); const shouldInit = await confirm({ message: "Would you like to create a manifest.json file?", @@ -135,6 +146,15 @@ export async function packExtension({ mcpbIgnorePatterns, ); + // When using a custom manifest path, inject the manifest into the bundle + if (customManifestPath) { + const manifestStat = statSync(manifestPath); + files["manifest.json"] = { + data: readFileSync(manifestPath), + mode: manifestStat.mode, + }; + } + // Print package header logger.log(`\n📦 ${manifest.name}@${manifest.version}`); diff --git a/test/cli.test.ts b/test/cli.test.ts index e7757ac..2586d7c 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -258,6 +258,84 @@ describe("DXT CLI", () => { expect(originalFile2).toEqual(unpackedFile2); }); + it("should pack with --manifest pointing to a separate directory", () => { + const projectDir = join(__dirname, "temp-manifest-project"); + const manifestDir = join(__dirname, "temp-manifest-separate"); + const manifestPackedPath = join(__dirname, "test-manifest-flag.mcpb"); + + try { + // Create project directory with source files (no manifest) + fs.mkdirSync(join(projectDir, "server"), { recursive: true }); + fs.writeFileSync( + join(projectDir, "server", "index.js"), + "console.log('hello');", + ); + + // Create separate manifest directory + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + join(manifestDir, "manifest.json"), + JSON.stringify({ + manifest_version: DEFAULT_MANIFEST_VERSION, + name: "Test Separate Manifest", + version: "1.0.0", + description: "Test with separate manifest", + author: { name: "MCPB" }, + server: { + type: "node", + entry_point: "server/index.js", + mcp_config: { command: "node" }, + }, + }), + ); + + const result = execSync( + `node ${cliPath} pack ${projectDir} ${manifestPackedPath} --manifest ${join(manifestDir, "manifest.json")}`, + { encoding: "utf-8" }, + ); + + expect(fs.existsSync(manifestPackedPath)).toBe(true); + expect(result).toContain("Validating manifest"); + } finally { + fs.rmSync(projectDir, { recursive: true, force: true }); + fs.rmSync(manifestDir, { recursive: true, force: true }); + if (fs.existsSync(manifestPackedPath)) { + fs.unlinkSync(manifestPackedPath); + } + } + }); + + it("should fail with --manifest pointing to nonexistent file", () => { + const projectDir = join(__dirname, "temp-manifest-missing-project"); + + try { + fs.mkdirSync(projectDir, { recursive: true }); + fs.writeFileSync(join(projectDir, "index.js"), "console.log('hello');"); + + expect(() => { + execSync( + `node ${cliPath} pack ${projectDir} /tmp/out.mcpb --manifest /nonexistent/manifest.json`, + { encoding: "utf-8", stdio: "pipe" }, + ); + }).toThrow(); + + try { + execSync( + `node ${cliPath} pack ${projectDir} /tmp/out.mcpb --manifest /nonexistent/manifest.json`, + { encoding: "utf-8", stdio: "pipe" }, + ); + } catch (error: unknown) { + const execError = error as { stdout?: Buffer; stderr?: Buffer }; + const output = + (execError.stdout?.toString() || "") + + (execError.stderr?.toString() || ""); + expect(output).toContain("Manifest file not found"); + } + } finally { + fs.rmSync(projectDir, { recursive: true, force: true }); + } + }); + it("should preserve executable file permissions after packing and unpacking", () => { // Skip this test on Windows since it doesn't support Unix permissions if (process.platform === "win32") { From f14863bcdcb090c5cf9556c718cdcc18aa77e5b5 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Sun, 15 Mar 2026 06:56:55 -0500 Subject: [PATCH 49/55] feat: enhanced structural validation for mcpb validate (#57) Add three pre-pack validators to catch common runtime failures early: - validateEntryPoint: checks file exists, extension matches server type, binary has executable bit (Unix) - validateCommandVariables: scans mcp_config for unrecognized ${...} variables against the runtime allowlist - validateSensitiveFiles: warns about credentials.json, *.pem, *.key, .ssh/, etc. that would be bundled validateManifest() now accepts optional { projectDir } to separate the manifest directory from the source file directory. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli/pack.ts | 6 +- src/node/validate.ts | 246 ++++++++++++++++++++++- test/cli.test.ts | 11 ++ test/icon-validation.test.ts | 4 + test/server/index.js | 1 + test/validate.test.ts | 369 +++++++++++++++++++++++++++++++++++ 6 files changed, 629 insertions(+), 8 deletions(-) create mode 100644 test/server/index.js create mode 100644 test/validate.test.ts diff --git a/src/cli/pack.ts b/src/cli/pack.ts index e2411bf..d2aba54 100644 --- a/src/cli/pack.ts +++ b/src/cli/pack.ts @@ -83,7 +83,11 @@ export async function packExtension({ // Validate manifest first logger.log("Validating manifest..."); - if (!validateManifest(manifestPath)) { + if ( + !validateManifest(manifestPath, { + projectDir: resolvedPath, + }) + ) { logger.error("ERROR: Cannot pack extension with invalid manifest"); return false; } diff --git a/src/node/validate.ts b/src/node/validate.ts index d1ee8fd..b958798 100644 --- a/src/node/validate.ts +++ b/src/node/validate.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync, statSync } from "fs"; import * as fs from "fs/promises"; import { DestroyerOfModules } from "galactus"; import * as os from "os"; -import { dirname, isAbsolute, join, resolve } from "path"; +import { dirname, extname, isAbsolute, join, resolve } from "path"; import prettyBytes from "pretty-bytes"; import { unpackExtension } from "../cli/unpack.js"; @@ -11,6 +11,7 @@ import { MANIFEST_SCHEMAS_LOOSE, } from "../shared/constants.js"; import { getManifestVersionFromRawData } from "../shared/manifestVersionResolve.js"; +import { getAllFilesWithCount, readMcpbIgnorePatterns } from "./files.js"; /** * Check if a buffer contains a valid PNG file signature @@ -108,7 +109,191 @@ function validateIcon( }; } -export function validateManifest(inputPath: string): boolean { +interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} + +// Expected file extensions by server type +const NODE_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]); +const PYTHON_EXTENSIONS = new Set([".py"]); +const SCRIPT_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".py"]); + +/** + * Validate that the server entry_point file exists and matches the server type + */ +function validateEntryPoint( + manifest: { server: { type: string; entry_point: string } }, + baseDir: string, +): ValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + const { type, entry_point } = manifest.server; + const entryPath = join(baseDir, entry_point); + + if (!existsSync(entryPath)) { + errors.push(`Entry point file not found: ${entry_point}`); + return { valid: false, errors, warnings }; + } + + const ext = extname(entry_point).toLowerCase(); + + if (type === "node" && !NODE_EXTENSIONS.has(ext)) { + warnings.push( + `Unusual entry point extension "${ext}" for server type "node". Expected: .js, .mjs, or .cjs`, + ); + } else if ( + (type === "python" || type === "uv") && + !PYTHON_EXTENSIONS.has(ext) + ) { + warnings.push( + `Unusual entry point extension "${ext}" for server type "${type}". Expected: .py`, + ); + } else if (type === "binary" && SCRIPT_EXTENSIONS.has(ext)) { + warnings.push( + `Entry point has script extension "${ext}" but server type is "binary". Did you mean type "node" or "python"?`, + ); + } + + // For binary type on Unix, check executable bit + if (type === "binary" && process.platform !== "win32") { + const stat = statSync(entryPath); + if (!(stat.mode & 0o111)) { + errors.push( + `Binary entry point is not executable: ${entry_point}. Run: chmod +x ${entry_point}`, + ); + } + } + + return { valid: errors.length === 0, errors, warnings }; +} + +// Valid variable patterns from src/shared/config.ts replaceVariables() +const VALID_VARIABLE_PATTERN = + /^\$\{(__dirname|pathSeparator|\/|user_config\..+)\}$/; + +/** + * Validate that ${...} variables in mcp_config are recognized + */ +function validateCommandVariables(manifest: { + server: { + mcp_config: { + command?: string; + args?: string[]; + env?: Record; + platform_overrides?: Record< + string, + { + command?: string; + args?: string[]; + env?: Record; + } + >; + }; + }; +}): ValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + + function checkString(value: string, context: string): void { + const variablePattern = /\$\{([^}]+)\}/g; + let match; + while ((match = variablePattern.exec(value)) !== null) { + const fullVar = match[0]; + if (!VALID_VARIABLE_PATTERN.test(fullVar)) { + errors.push( + `Invalid variable "${fullVar}" in ${context}. Valid variables: \${__dirname}, \${pathSeparator}, \${/}, \${user_config.}`, + ); + } + } + } + + function checkConfig( + config: { command?: string; args?: string[]; env?: Record }, + prefix: string, + ): void { + if (config.command) checkString(config.command, `${prefix}command`); + if (config.args) { + config.args.forEach((arg, i) => checkString(arg, `${prefix}args[${i}]`)); + } + if (config.env) { + for (const [key, val] of Object.entries(config.env)) { + checkString(val, `${prefix}env.${key}`); + } + } + } + + const { mcp_config } = manifest.server; + checkConfig(mcp_config, "mcp_config."); + + if (mcp_config.platform_overrides) { + for (const [platform, override] of Object.entries( + mcp_config.platform_overrides, + )) { + checkConfig(override, `mcp_config.platform_overrides.${platform}.`); + } + } + + return { valid: errors.length === 0, errors, warnings }; +} + +// Sensitive file patterns not already covered by EXCLUDE_PATTERNS in files.ts +const SENSITIVE_PATTERNS = [ + /(^|\/)credentials\.json$/i, + /(^|\/)secrets\./i, + /\.pem$/i, + /\.key$/i, + /\.p12$/i, + /\.pfx$/i, + /\.jks$/i, + /(^|\/)\.aws\//, + /(^|\/)\.ssh\//, + /(^|\/)id_rsa/, + /(^|\/)id_ed25519/, + /(^|\/)id_ecdsa/, + /\.keystore$/i, + /(^|\/)token\.json$/i, +]; + +/** + * Check if the file list that would be bundled contains sensitive files + */ +function validateSensitiveFiles(baseDir: string): ValidationResult { + const warnings: string[] = []; + + try { + const mcpbIgnorePatterns = readMcpbIgnorePatterns(baseDir); + const { files } = getAllFilesWithCount( + baseDir, + baseDir, + {}, + mcpbIgnorePatterns, + ); + + for (const filePath of Object.keys(files)) { + for (const pattern of SENSITIVE_PATTERNS) { + if (pattern.test(filePath)) { + warnings.push( + `Potentially sensitive file will be included in bundle: ${filePath}`, + ); + break; + } + } + } + } catch { + // If we can't read the directory, skip this check silently — + // pack will fail with a clearer error later + } + + // Sensitive files are always warnings, never errors — a .pem might be a legitimate TLS cert + return { valid: true, errors: [], warnings }; +} + +export function validateManifest( + inputPath: string, + options?: { projectDir?: string }, +): boolean { try { const resolvedPath = resolve(inputPath); let manifestPath = resolvedPath; @@ -131,17 +316,23 @@ export function validateManifest(inputPath: string): boolean { if (result.success) { console.log("Manifest schema validation passes!"); - // Validate icon if present + const manifestDir = dirname(manifestPath); + // projectDir is where source files live — defaults to the manifest's directory + const projectDir = options?.projectDir + ? resolve(options.projectDir) + : manifestDir; + let hasErrors = false; + + // Validate icon if present (always relative to manifest directory) if (manifestData.icon) { - const baseDir = dirname(manifestPath); - const iconValidation = validateIcon(manifestData.icon, baseDir); + const iconValidation = validateIcon(manifestData.icon, manifestDir); if (iconValidation.errors.length > 0) { console.log("\nERROR: Icon validation failed:\n"); iconValidation.errors.forEach((error) => { console.log(` - ${error}`); }); - return false; + hasErrors = true; } if (iconValidation.warnings.length > 0) { @@ -152,7 +343,48 @@ export function validateManifest(inputPath: string): boolean { } } - return true; + // Validate entry point (relative to project directory) + const entryPointValidation = validateEntryPoint(manifestData, projectDir); + if (entryPointValidation.errors.length > 0) { + console.log("\nERROR: Entry point validation failed:\n"); + entryPointValidation.errors.forEach((error) => { + console.log(` - ${error}`); + }); + hasErrors = true; + } + if (entryPointValidation.warnings.length > 0) { + console.log("\nEntry point warnings:\n"); + entryPointValidation.warnings.forEach((warning) => { + console.log(` - ${warning}`); + }); + } + + // Validate command variables + const variableValidation = validateCommandVariables(manifestData); + if (variableValidation.errors.length > 0) { + console.log("\nERROR: Command variable validation failed:\n"); + variableValidation.errors.forEach((error) => { + console.log(` - ${error}`); + }); + hasErrors = true; + } + if (variableValidation.warnings.length > 0) { + console.log("\nCommand variable warnings:\n"); + variableValidation.warnings.forEach((warning) => { + console.log(` - ${warning}`); + }); + } + + // Check for sensitive files (relative to project directory) + const sensitiveValidation = validateSensitiveFiles(projectDir); + if (sensitiveValidation.warnings.length > 0) { + console.log("\nSensitive file warnings:\n"); + sensitiveValidation.warnings.forEach((warning) => { + console.log(` - ${warning}`); + }); + } + + return !hasErrors; } else { console.log("ERROR: Manifest validation failed:\n"); result.error.issues.forEach((issue) => { diff --git a/test/cli.test.ts b/test/cli.test.ts index e7757ac..7ecc402 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -99,6 +99,10 @@ describe("DXT CLI", () => { 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, ]); fs.writeFileSync(join(testDir, "icon.png"), validPngBuffer); + + // Create entry point file referenced by test manifests + fs.mkdirSync(join(testDir, "server"), { recursive: true }); + fs.writeFileSync(join(testDir, "server", "index.js"), "// fixture"); }); afterAll(() => { @@ -196,6 +200,9 @@ describe("DXT CLI", () => { fs.writeFileSync(join(tempDir, "file1.txt"), "hello"); fs.mkdirSync(join(tempDir, "subdir")); fs.writeFileSync(join(tempDir, "subdir", "file2.txt"), "world"); + // Create entry point file referenced by manifest + fs.mkdirSync(join(tempDir, "server"), { recursive: true }); + fs.writeFileSync(join(tempDir, "server", "index.js"), "// fixture"); }); afterAll(() => { @@ -291,6 +298,10 @@ describe("DXT CLI", () => { }), ); + // Create entry point file referenced by manifest + fs.mkdirSync(join(tempExecDir, "server"), { recursive: true }); + fs.writeFileSync(join(tempExecDir, "server", "index.js"), "// fixture"); + // Create an executable script const executableScript = join(tempExecDir, "run-script.sh"); fs.writeFileSync( diff --git a/test/icon-validation.test.ts b/test/icon-validation.test.ts index b55abd4..bdc7bbe 100644 --- a/test/icon-validation.test.ts +++ b/test/icon-validation.test.ts @@ -93,6 +93,10 @@ describe("Icon Validation", () => { "Not a PNG file", ); + // Create entry point file referenced by test manifests + fs.mkdirSync(join(testFixturesDir, "server"), { recursive: true }); + fs.writeFileSync(join(testFixturesDir, "server", "index.js"), "// fixture"); + // Create test manifests createTestManifest("valid-local-icon.json", { icon: "valid-icon.png", diff --git a/test/server/index.js b/test/server/index.js new file mode 100644 index 0000000..c8a782c --- /dev/null +++ b/test/server/index.js @@ -0,0 +1 @@ +// test fixture diff --git a/test/validate.test.ts b/test/validate.test.ts new file mode 100644 index 0000000..e998fb2 --- /dev/null +++ b/test/validate.test.ts @@ -0,0 +1,369 @@ +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import { join } from "node:path"; + +import { DEFAULT_MANIFEST_VERSION } from "../src/shared/constants"; + +interface ExecSyncError extends Error { + stdout: Buffer; + stderr: Buffer; + status: number | null; +} + +describe("Enhanced Validation", () => { + const cliPath = join(__dirname, "../dist/cli/cli.js"); + const fixturesDir = join(__dirname, "fixtures", "validate"); + + function createManifest( + dir: string, + overrides: Record = {}, + ) { + const manifest = { + manifest_version: DEFAULT_MANIFEST_VERSION, + name: "test-extension", + version: "1.0.0", + description: "Test extension for validation", + author: { name: "Test Author" }, + server: { + type: "node", + entry_point: "server/index.js", + mcp_config: { + command: "node", + args: ["${__dirname}/server/index.js"], + }, + }, + ...overrides, + }; + + fs.writeFileSync( + join(dir, "manifest.json"), + JSON.stringify(manifest, null, 2), + ); + } + + beforeAll(() => { + execSync("yarn build", { cwd: join(__dirname, "..") }); + + if (fs.existsSync(fixturesDir)) { + fs.rmSync(fixturesDir, { recursive: true, force: true }); + } + fs.mkdirSync(fixturesDir, { recursive: true }); + }); + + afterAll(() => { + if (fs.existsSync(fixturesDir)) { + fs.rmSync(fixturesDir, { recursive: true, force: true }); + } + }); + + describe("entry point validation", () => { + it("should fail when entry_point file is missing", () => { + const dir = join(fixturesDir, "missing-entry"); + fs.mkdirSync(dir, { recursive: true }); + createManifest(dir); + // Deliberately NOT creating server/index.js + + expect(() => { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + }).toThrow(); + + try { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + } catch (error: unknown) { + const execError = error as ExecSyncError; + const output = execError.stdout?.toString() || ""; + expect(output).toContain("Entry point file not found"); + } + }); + + it("should warn about extension mismatch (node type with .py file)", () => { + const dir = join(fixturesDir, "ext-mismatch"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "main.py"), "print('hello')"); + createManifest(dir, { + server: { + type: "node", + entry_point: "server/main.py", + mcp_config: { command: "node" }, + }, + }); + + const result = execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + }); + + expect(result).toContain("Manifest schema validation passes!"); + expect(result).toContain("Unusual entry point extension"); + }); + + it("should fail when binary entry_point is not executable (Unix only)", () => { + if (process.platform === "win32") { + return; + } + + const dir = join(fixturesDir, "binary-not-exec"); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(join(dir, "myserver"), "#!/bin/sh\necho hi"); + fs.chmodSync(join(dir, "myserver"), 0o644); + createManifest(dir, { + server: { + type: "binary", + entry_point: "myserver", + mcp_config: { command: "./myserver" }, + }, + }); + + expect(() => { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + }).toThrow(); + + try { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + } catch (error: unknown) { + const execError = error as ExecSyncError; + const output = execError.stdout?.toString() || ""; + expect(output).toContain("not executable"); + } + }); + + it("should pass when binary entry_point is executable (Unix only)", () => { + if (process.platform === "win32") { + return; + } + + const dir = join(fixturesDir, "binary-exec"); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(join(dir, "myserver"), "#!/bin/sh\necho hi"); + fs.chmodSync(join(dir, "myserver"), 0o755); + createManifest(dir, { + server: { + type: "binary", + entry_point: "myserver", + mcp_config: { command: "./myserver" }, + }, + }); + + const result = execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + }); + + expect(result).toContain("Manifest schema validation passes!"); + }); + }); + + describe("command variable validation", () => { + it("should fail on invalid variables like ${BUNDLE_ROOT}", () => { + const dir = join(fixturesDir, "invalid-var"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + createManifest(dir, { + server: { + type: "node", + entry_point: "server/index.js", + mcp_config: { + command: "node", + args: ["${BUNDLE_ROOT}/server/index.js"], + }, + }, + }); + + expect(() => { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + }).toThrow(); + + try { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + } catch (error: unknown) { + const execError = error as ExecSyncError; + const output = execError.stdout?.toString() || ""; + expect(output).toContain("Invalid variable"); + expect(output).toContain("${BUNDLE_ROOT}"); + } + }); + + it("should fail on invalid variables in env values", () => { + const dir = join(fixturesDir, "invalid-env-var"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + createManifest(dir, { + server: { + type: "node", + entry_point: "server/index.js", + mcp_config: { + command: "node", + args: ["${__dirname}/server/index.js"], + env: { PATH: "${HOME}/bin" }, + }, + }, + }); + + expect(() => { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + }).toThrow(); + + try { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + } catch (error: unknown) { + const execError = error as ExecSyncError; + const output = execError.stdout?.toString() || ""; + expect(output).toContain("Invalid variable"); + expect(output).toContain("${HOME}"); + } + }); + + it("should pass with valid variables: ${__dirname} and ${user_config.*}", () => { + const dir = join(fixturesDir, "valid-vars"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + createManifest(dir, { + server: { + type: "node", + entry_point: "server/index.js", + mcp_config: { + command: "node", + args: [ + "${__dirname}/server/index.js", + "--key", + "${user_config.api_key}", + ], + env: { SEP: "${pathSeparator}" }, + }, + }, + }); + + const result = execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + }); + + expect(result).toContain("Manifest schema validation passes!"); + expect(result).not.toContain("Invalid variable"); + }); + + it("should fail on invalid variables in platform_overrides", () => { + const dir = join(fixturesDir, "invalid-platform-var"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + + const manifest = { + manifest_version: "0.3", + name: "test-extension", + version: "1.0.0", + description: "Test", + author: { name: "Test" }, + server: { + type: "node", + entry_point: "server/index.js", + mcp_config: { + command: "node", + args: ["${__dirname}/server/index.js"], + platform_overrides: { + win32: { + args: ["${APPDATA}/server/index.js"], + }, + }, + }, + }, + }; + + fs.writeFileSync( + join(dir, "manifest.json"), + JSON.stringify(manifest, null, 2), + ); + + expect(() => { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + }).toThrow(); + + try { + execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + stdio: "pipe", + }); + } catch (error: unknown) { + const execError = error as ExecSyncError; + const output = execError.stdout?.toString() || ""; + expect(output).toContain("Invalid variable"); + expect(output).toContain("${APPDATA}"); + } + }); + }); + + describe("sensitive file detection", () => { + it("should warn about credentials.json in project directory", () => { + const dir = join(fixturesDir, "sensitive-files"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + fs.writeFileSync(join(dir, "credentials.json"), '{"secret": "value"}'); + createManifest(dir); + + const result = execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + }); + + // Should pass (warnings only) but show the warning + expect(result).toContain("Manifest schema validation passes!"); + expect(result).toContain("Potentially sensitive file"); + expect(result).toContain("credentials.json"); + }); + + it("should warn about .pem files in project directory", () => { + const dir = join(fixturesDir, "sensitive-pem"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); + fs.writeFileSync(join(dir, "server.pem"), "--- CERT ---"); + createManifest(dir); + + const result = execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + }); + + expect(result).toContain("Manifest schema validation passes!"); + expect(result).toContain("Potentially sensitive file"); + expect(result).toContain("server.pem"); + }); + }); + + describe("happy path", () => { + it("should pass with all files present and correct types", () => { + const dir = join(fixturesDir, "happy-path"); + fs.mkdirSync(join(dir, "server"), { recursive: true }); + fs.writeFileSync(join(dir, "server", "index.js"), "// server code"); + createManifest(dir); + + const result = execSync(`node ${cliPath} validate ${dir}`, { + encoding: "utf-8", + }); + + expect(result).toContain("Manifest schema validation passes!"); + expect(result).not.toContain("ERROR"); + expect(result).not.toContain("sensitive"); + }); + }); +}); From cb052a20e50f71cf946f7ad49141492f990810c5 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Thu, 26 Mar 2026 08:23:18 -0500 Subject: [PATCH 50/55] fix: merge platform_overrides env with base env instead of replacing platform_overrides.*.env was replacing the entire mcp_config.env object instead of merging, causing base env vars (including ${user_config.*} references) to be silently dropped when platform overrides defined any env vars. Fixes #217 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/shared/config.ts | 4 +++- test/config.test.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/shared/config.ts b/src/shared/config.ts index b4638bc..7073f58 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -117,7 +117,9 @@ export async function getMcpConfigForManifest( result.command = platformConfig.command || result.command; result.args = platformConfig.args || result.args; - result.env = platformConfig.env || result.env; + result.env = platformConfig.env + ? { ...result.env, ...platformConfig.env } + : result.env; } } diff --git a/test/config.test.ts b/test/config.test.ts index 1c34faf..7e29f69 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -269,6 +269,54 @@ describe("getMcpConfigForManifest", () => { expect(result?.args).toEqual(["server.js", "/user/path1", "/user/path2"]); }); + it("should merge platform override env with base env instead of replacing", async () => { + const manifest: McpbManifestAny = { + ...baseManifest, + user_config: { + data_directory: { + type: "directory", + title: "Data Directory", + description: "Where data lives", + required: true, + }, + }, + server: { + type: "node", + entry_point: "server.js", + mcp_config: { + command: "node", + args: ["server.js"], + env: { + DATA_DIR: "${user_config.data_directory}", + EXISTING_VAR: "keep-me", + }, + platform_overrides: { + [process.platform]: { + env: { + PATH: "/usr/local/bin:${HOME}/.local/bin", + }, + }, + }, + }, + }, + }; + + const result = await getMcpConfigForManifest({ + manifest, + extensionPath: "/ext/path", + systemDirs: { ...mockSystemDirs, HOME: "/home/user" }, + userConfig: { data_directory: "/custom/data" }, + pathSeparator: "/", + logger: mockLogger, + }); + + // Base env vars should be preserved after platform override merge + expect(result?.env?.DATA_DIR).toBe("/custom/data"); + expect(result?.env?.EXISTING_VAR).toBe("keep-me"); + // Platform override env should also be present + expect(result?.env?.PATH).toBe("/usr/local/bin:/home/user/.local/bin"); + }); + it("should convert boolean user config values to strings", async () => { const manifest: McpbManifestAny = { ...baseManifest, From 122ef0e9e98bfaf70944090acfc09323aa234aa9 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Fri, 27 Mar 2026 09:18:30 -0500 Subject: [PATCH 51/55] fix: enforce executable permissions on binary entry points during pack When server.type is "binary", mcpb pack now ensures the entry_point file has execute bits (|0o111) in the ZIP external_attr, regardless of the source filesystem permissions. This prevents EACCES failures when Claude Desktop extracts Windows-built binary MCPBs (which store 644 because Windows has no Unix execute bit). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli/pack.ts | 15 +++++++--- test/cli.test.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/cli/pack.ts b/src/cli/pack.ts index d2aba54..b4d0be9 100644 --- a/src/cli/pack.ts +++ b/src/cli/pack.ts @@ -206,15 +206,22 @@ export async function packExtension({ const zipFiles: Zippable = {}; const isUnix = process.platform !== "win32"; + const isBinaryType = manifest.server?.type === "binary"; + const binaryEntryPoint = isBinaryType ? manifest.server.entry_point : null; for (const [filePath, fileData] of Object.entries(files)) { if (isUnix) { + let mode = fileData.mode & 0o777; + // Ensure binary entry points are always executable in the ZIP, + // regardless of filesystem permissions. Windows-built ZIPs store + // 644 because Windows has no Unix execute bit — CD spawns binary + // entry points directly, so they must have +x to avoid EACCES. + if (binaryEntryPoint && filePath === binaryEntryPoint) { + mode = mode | 0o111; + } // Set external file attributes to preserve Unix permissions // The mode needs to be shifted to the upper 16 bits for ZIP format - zipFiles[filePath] = [ - fileData.data, - { os: 3, attrs: (fileData.mode & 0o777) << 16 }, - ]; + zipFiles[filePath] = [fileData.data, { os: 3, attrs: mode << 16 }]; } else { // On Windows, use default ZIP attributes (no Unix permissions) zipFiles[filePath] = fileData.data; diff --git a/test/cli.test.ts b/test/cli.test.ts index 7ecc402..907a783 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -357,5 +357,82 @@ describe("DXT CLI", () => { } } }); + + it("should add group+other execute bits on binary entry points during pack", () => { + // Skip this test on Windows since it doesn't support Unix permissions + if (process.platform === "win32") { + return; + } + + const tempBinDir = join(__dirname, "temp-binary-entry-test"); + const binPackedFilePath = join(__dirname, "test-binary-entry.dxt"); + const binUnpackedDir = join(__dirname, "temp-binary-entry-unpack"); + + try { + // Create a binary-type extension with owner-only execute (0o700) + // This passes validation (has some +x) but pack should add + // group+other execute bits for compatibility with CD extraction + fs.mkdirSync(join(tempBinDir, "server"), { recursive: true }); + fs.writeFileSync( + join(tempBinDir, "manifest.json"), + JSON.stringify({ + manifest_version: DEFAULT_MANIFEST_VERSION, + name: "Test Binary Extension", + version: "1.0.0", + description: "A test extension with binary entry point", + author: { + name: "MCPB", + }, + server: { + type: "binary", + entry_point: "server/myserver", + mcp_config: { + command: "${__dirname}/server/myserver", + }, + }, + }), + ); + + // Create binary entry point with 700 (owner-only execute) + fs.writeFileSync( + join(tempBinDir, "server", "myserver"), + "#!/bin/sh\necho binary", + ); + fs.chmodSync(join(tempBinDir, "server", "myserver"), 0o700); + + // Create a regular file for comparison (should stay 644) + fs.writeFileSync(join(tempBinDir, "config.json"), "{}"); + fs.chmodSync(join(tempBinDir, "config.json"), 0o644); + + // Pack the extension + execSync(`node ${cliPath} pack ${tempBinDir} ${binPackedFilePath}`, { + encoding: "utf-8", + }); + + // Unpack to check stored permissions + execSync( + `node ${cliPath} unpack ${binPackedFilePath} ${binUnpackedDir}`, + { + encoding: "utf-8", + }, + ); + + // Binary entry point should have full execute bits (700 | 111 = 711) + const entryStats = fs.statSync( + join(binUnpackedDir, "server", "myserver"), + ); + expect(entryStats.mode & 0o777).toBe(0o711); + + // Non-entry-point file should retain original permissions + const configStats = fs.statSync(join(binUnpackedDir, "config.json")); + expect(configStats.mode & 0o777).toBe(0o644); + } finally { + fs.rmSync(tempBinDir, { recursive: true, force: true }); + fs.rmSync(binUnpackedDir, { recursive: true, force: true }); + if (fs.existsSync(binPackedFilePath)) { + fs.unlinkSync(binPackedFilePath); + } + } + }); }); }); From dace08d31fa89e7026bcee17d4e85407a580e311 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Fri, 17 Apr 2026 13:49:30 -0500 Subject: [PATCH 52/55] fix(examples/chrome-applescript): sanitize tab_id to prevent AppleScript injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inputSchema `type: "number"` is advisory only — runtime may receive a string payload that gets interpolated into the osascript template. Coerce tab_id via Number.parseInt before interpolating at all four sites. Affected sites: - findTabById(tabId) - executeTabOperation(tabId, ...) (close_tab, switch_to_tab, reload_tab, go_back, go_forward) - execute_javascript handler - get_page_content handler The shared toSafeTabId() helper returns null for missing values (preserving the active-tab fallthrough) and throws on non-numeric input (surfacing misuse via the existing request-level try/catch, consistent with the missing-code path in execute_javascript). --- examples/chrome-applescript/server/index.js | 43 +++++++++++++++------ 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/examples/chrome-applescript/server/index.js b/examples/chrome-applescript/server/index.js index dd96bdd..8ffb778 100644 --- a/examples/chrome-applescript/server/index.js +++ b/examples/chrome-applescript/server/index.js @@ -82,12 +82,28 @@ class ChromeControlServer { } } + // Coerce tab_id to a safe numeric value before interpolating into + // AppleScript. inputSchema type:"number" is advisory — runtime may receive + // a string, which would otherwise be injected into the osascript template. + toSafeTabId(tabId) { + if (tabId === null || tabId === undefined) return null; + const n = Number.parseInt(tabId, 10); + if (Number.isNaN(n)) { + throw new Error("tab_id must be a number"); + } + return n; + } + async findTabById(tabId) { + const safeTabId = this.toSafeTabId(tabId); + if (safeTabId === null) { + return { w: null, t: null, found: false }; + } const script = ` tell application "Google Chrome" repeat with w in windows repeat with t in tabs of w - if (id of t as string) is "${tabId}" then + if (id of t as string) is "${safeTabId}" then return {w, t, true} end if end repeat @@ -103,7 +119,8 @@ class ChromeControlServer { operation, successMessage = "Operation completed", ) { - if (!tabId) { + const safeTabId = this.toSafeTabId(tabId); + if (safeTabId === null) { // No tab ID, execute on active tab return this.executeAppleScript(operation.activeScript); } @@ -112,7 +129,7 @@ class ChromeControlServer { tell application "Google Chrome" repeat with w in windows repeat with t in tabs of w - if (id of t as string) is "${tabId}" then + if (id of t as string) is "${safeTabId}" then ${operation.tabScript} return "${successMessage}" end if @@ -551,6 +568,7 @@ class ChromeControlServer { "JavaScript code is required and must be a string", ); } + const safeTabId = this.toSafeTabId(tab_id); // Wrap the code to capture console.log outputs const wrappedCode = ` @@ -628,12 +646,13 @@ class ChromeControlServer { const escapedCode = this.escapeForAppleScript(wrappedCode); - const script = tab_id - ? ` + const script = + safeTabId != null + ? ` tell application "Google Chrome" repeat with w in windows repeat with t in tabs of w - if (id of t as string) is "${tab_id}" then + if (id of t as string) is "${safeTabId}" then set result to execute t javascript "${escapedCode}" return result end if @@ -642,7 +661,7 @@ class ChromeControlServer { return "Tab not found" end tell ` - : ` + : ` tell application "Google Chrome" execute active tab of front window javascript "${escapedCode}" end tell @@ -695,14 +714,16 @@ class ChromeControlServer { case "get_page_content": { const { tab_id } = args; + const safeTabId = this.toSafeTabId(tab_id); const jsCode = "document.body.innerText"; - const script = tab_id - ? ` + const script = + safeTabId != null + ? ` tell application "Google Chrome" repeat with w in windows repeat with t in tabs of w - if (id of t as string) is "${tab_id}" then + if (id of t as string) is "${safeTabId}" then set pageContent to execute t javascript "${jsCode}" return pageContent end if @@ -711,7 +732,7 @@ class ChromeControlServer { return "Tab not found" end tell ` - : ` + : ` tell application "Google Chrome" execute active tab of front window javascript "${jsCode}" end tell From 73f8475a80d48df534549d231d162c43ae1a9e80 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Fri, 17 Apr 2026 14:22:56 -0500 Subject: [PATCH 53/55] fix(examples/chrome-applescript): use JSON.stringify for AppleScript string interpolation --- examples/chrome-applescript/server/index.js | 36 ++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/chrome-applescript/server/index.js b/examples/chrome-applescript/server/index.js index 8ffb778..4f19b11 100644 --- a/examples/chrome-applescript/server/index.js +++ b/examples/chrome-applescript/server/index.js @@ -33,17 +33,6 @@ class ChromeControlServer { this.setupHandlers(); } - // Helper methods - escapeForAppleScript(str) { - if (typeof str !== "string") return str; - // Basic AppleScript string escaping - return str - .replace(/\\/g, "\\\\") // Escape backslashes first - .replace(/"/g, '\\"') // Then escape double quotes - .replace(/\n/g, "\\n") // Escape newlines - .replace(/\r/g, "\\r"); // Escape carriage returns - } - async checkChromeAvailable() { try { const script = 'tell application "Google Chrome" to return "available"'; @@ -289,10 +278,23 @@ class ChromeControlServer { throw new Error("URL is required and must be a string"); } - const escapedUrl = this.escapeForAppleScript(url); + try { + new URL(url); + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: url is not a valid url - ${error.message}`, + }, + ], + isError: true, + }; + } + const script = new_tab - ? `tell application "Google Chrome" to open location "${escapedUrl}"` - : `tell application "Google Chrome" to set URL of active tab of front window to "${escapedUrl}"`; + ? `tell application "Google Chrome" to open location ${JSON.stringify(url)}` + : `tell application "Google Chrome" to set URL of active tab of front window to ${JSON.stringify(url)}`; await this.executeAppleScript(script); return { @@ -644,8 +646,6 @@ class ChromeControlServer { })(); `; - const escapedCode = this.escapeForAppleScript(wrappedCode); - const script = safeTabId != null ? ` @@ -653,7 +653,7 @@ class ChromeControlServer { repeat with w in windows repeat with t in tabs of w if (id of t as string) is "${safeTabId}" then - set result to execute t javascript "${escapedCode}" + set result to execute t javascript ${JSON.stringify(wrappedCode)} return result end if end repeat @@ -663,7 +663,7 @@ class ChromeControlServer { ` : ` tell application "Google Chrome" - execute active tab of front window javascript "${escapedCode}" + execute active tab of front window javascript ${JSON.stringify(wrappedCode)} end tell `; From 5161613c798c4d6d568155eaab4e116e709f3b3e Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Wed, 22 Apr 2026 10:51:05 -0500 Subject: [PATCH 54/55] docs: add "Publishing Your Bundle" section to README Covers the recommended distribution pattern (GitHub Release assets), tag convention for monorepos, per-platform asset naming, compatibility. platforms correctness, a minimum CI shape, and ${__dirname} guidance for relocatable entry points. --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/README.md b/README.md index d9f1fbe..1fb6fde 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,81 @@ bundle.mcpb (ZIP file) - Include all required shared libraries if dynamic linking used - Test on clean systems without development tools +## Publishing Your Bundle + +Once you've packed an `.mcpb`, you need a way to distribute it. The recommended pattern is to attach it as an asset on a GitHub Release — tagged, versioned, and addressable without any additional hosting. + +### Why GitHub Releases + +- Release tags give consumers a stable, versioned URL per bundle. +- Standard tooling (`gh`, `curl`, release-feed polling) can pull assets directly. +- Monorepos can ship multiple bundles under different tag prefixes. +- No extra infrastructure to run. + +### Tag Convention + +Use a prefixed tag pattern so a single repository can ship more than one bundle: + +``` +- +``` + +Examples: + +``` +my-company-mcp-1.3.0 +analytics-mcp-2.0.1 +``` + +### Per-Platform Assets + +If your server bundles platform-specific binaries, attach one `.mcpb` per platform with a consistent naming suffix: + +``` +--darwin-arm64.mcpb +--darwin-x64.mcpb +--win32-x64.mcpb +``` + +If your server is cross-platform (Node.js, Python, pure script), a single `.mcpb` per release is sufficient. + +Declare `compatibility.platforms` in each bundle's `manifest.json` to match what's actually in the bundle. Do not list platforms you haven't built for — consumers rely on this field to reject incompatible installs. + +### Uploading from CI + +After `mcpb pack` produces the bundle: + +```sh +gh release upload "$TAG" dist/*.mcpb --clobber +``` + +`--clobber` keeps the release job idempotent on re-run. + +### Minimum CI Shape + +Triggered by a tag push or release event, a workflow does four things in order: + +1. **Build** your server for each target platform. +2. **Stage** `manifest.json` + server code + `icon.png` + `LICENSE` into a bundle directory. +3. **Pack** with `mcpb pack ` to produce the `.mcpb`. +4. **Upload** to the GitHub Release via `gh release upload`. + +Signing, if required by your policy, slots in between steps 3 and 4. + +### Entry Point Paths + +`manifest.json` should set `entry_point` to a relative path inside the bundle, and `mcp_config.args` should reference it via `${__dirname}`: + +```json +"entry_point": "server/index.js", +"mcp_config": { + "command": "node", + "args": ["${__dirname}/server/index.js"] +} +``` + +This keeps the bundle relocatable — consumers install to arbitrary paths, and `${__dirname}` resolves to the extracted bundle root at runtime. + # Contributing We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. From b4acc3f73033b79dea4c8ef14fef1c1b6427f949 Mon Sep 17 00:00:00 2001 From: Tobin South Date: Wed, 22 Apr 2026 17:14:57 +0100 Subject: [PATCH 55/55] Revert "docs: add "Publishing Your Bundle" section to README" --- README.md | 75 ------------------------------------------------------- 1 file changed, 75 deletions(-) diff --git a/README.md b/README.md index 1fb6fde..d9f1fbe 100644 --- a/README.md +++ b/README.md @@ -139,81 +139,6 @@ bundle.mcpb (ZIP file) - Include all required shared libraries if dynamic linking used - Test on clean systems without development tools -## Publishing Your Bundle - -Once you've packed an `.mcpb`, you need a way to distribute it. The recommended pattern is to attach it as an asset on a GitHub Release — tagged, versioned, and addressable without any additional hosting. - -### Why GitHub Releases - -- Release tags give consumers a stable, versioned URL per bundle. -- Standard tooling (`gh`, `curl`, release-feed polling) can pull assets directly. -- Monorepos can ship multiple bundles under different tag prefixes. -- No extra infrastructure to run. - -### Tag Convention - -Use a prefixed tag pattern so a single repository can ship more than one bundle: - -``` -- -``` - -Examples: - -``` -my-company-mcp-1.3.0 -analytics-mcp-2.0.1 -``` - -### Per-Platform Assets - -If your server bundles platform-specific binaries, attach one `.mcpb` per platform with a consistent naming suffix: - -``` ---darwin-arm64.mcpb ---darwin-x64.mcpb ---win32-x64.mcpb -``` - -If your server is cross-platform (Node.js, Python, pure script), a single `.mcpb` per release is sufficient. - -Declare `compatibility.platforms` in each bundle's `manifest.json` to match what's actually in the bundle. Do not list platforms you haven't built for — consumers rely on this field to reject incompatible installs. - -### Uploading from CI - -After `mcpb pack` produces the bundle: - -```sh -gh release upload "$TAG" dist/*.mcpb --clobber -``` - -`--clobber` keeps the release job idempotent on re-run. - -### Minimum CI Shape - -Triggered by a tag push or release event, a workflow does four things in order: - -1. **Build** your server for each target platform. -2. **Stage** `manifest.json` + server code + `icon.png` + `LICENSE` into a bundle directory. -3. **Pack** with `mcpb pack ` to produce the `.mcpb`. -4. **Upload** to the GitHub Release via `gh release upload`. - -Signing, if required by your policy, slots in between steps 3 and 4. - -### Entry Point Paths - -`manifest.json` should set `entry_point` to a relative path inside the bundle, and `mcp_config.args` should reference it via `${__dirname}`: - -```json -"entry_point": "server/index.js", -"mcp_config": { - "command": "node", - "args": ["${__dirname}/server/index.js"] -} -``` - -This keeps the bundle relocatable — consumers install to arbitrary paths, and `${__dirname}` resolves to the extracted bundle root at runtime. - # Contributing We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.