Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions MANIFEST.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# MCPB Manifest.json Spec

Current version: `0.2`
Copy link
Contributor

@joan-anthropic joan-anthropic Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a PR open here to rename 0.3 -> vNext, but I think we want to avoid publicizing / documenting 0.3 until it's finalized (unless we think this PR captures all of the remaining requirements- i think we still have folder tokens?)

Copy link
Contributor Author

@asklar asklar Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joan-anthropic re: folder tokens - after thinking about it more, I think we don't need that just yet (we may in the future, but I wouldn't want to hold 0.3 for that). Are there other changes in 0.3 you would like to get in? happy to snap to whatever y'all need and rebase this on top of your 0.3->vNext PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good! v0.3 sounds good then

Last updated: 2025-09-12
Current version: `0.3`
Last updated: 2025-10-24

## Manifest Schema

Expand All @@ -11,7 +11,7 @@ A basic `manifest.json` with just the required fields looks like this:

```jsonc
{
"manifest_version": "0.2", // Manifest spec version this manifest conforms to
"manifest_version": "0.3", // Manifest spec version this manifest conforms to
"name": "my-extension", // Machine-readable name (used for CLI, APIs)
"version": "1.0.0", // Semantic version of your extension
"description": "A simple MCP extension", // Brief description of what the extension does
Expand All @@ -37,7 +37,7 @@ A basic `manifest.json` with just the required fields looks like this:

```json
{
"manifest_version": "0.2",
"manifest_version": "0.3",
"name": "my-extension",
"version": "1.0.0",
"description": "A simple MCP extension",
Expand Down Expand Up @@ -71,7 +71,7 @@ A full `manifest.json` with most of the optional fields looks like this:

```json
{
"manifest_version": "0.1",
"manifest_version": "0.3",
"name": "My MCP Extension",
"display_name": "My Awesome MCP Extension",
"version": "1.0.0",
Expand All @@ -90,10 +90,26 @@ A full `manifest.json` with most of the optional fields looks like this:
"documentation": "https://docs.example.com/my-extension",
"support": "https://github.com/your-username/my-extension/issues",
"icon": "icon.png",
"icons": [
{
"src": "assets/icons/icon-16-light.png",
"sizes": "16x16",
"theme": "light"
},
{
"src": "assets/icons/icon-16-dark.png",
"sizes": "16x16",
"theme": "dark"
}
],
"screenshots": [
"assets/screenshots/screenshot1.png",
"assets/screenshots/screenshot2.png"
],
"localization": {
"resources": "resources/${locale}.json",
"default_locale": "en-US"
},
"server": {
"type": "node",
"entry_point": "server/index.js",
Expand Down Expand Up @@ -221,6 +237,7 @@ A full `manifest.json` with most of the optional fields looks like this:
### Optional Fields

- **icon**: Path to a png icon file, either relative in the package or a https:// url.
- **icons**: Array of icon descriptors (`src`, `sizes`, optional `theme`) for light/dark or size-specific assets.
- **display_name**: Human-friendly name for UI display
- **long_description**: Detailed description for extension stores, markdown
- **repository**: Source code repository information (type and url)
Expand All @@ -238,6 +255,38 @@ A full `manifest.json` with most of the optional fields looks like this:
- **compatibility**: Compatibility requirements (client app version, platforms, and runtime versions)
- **user_config**: User-configurable options for the extension (see User Configuration section)
- **_meta**: Platform-specific client integration metadata (e.g., Windows `package_family_name`, macOS bundle identifiers) enabling tighter OS/app store integration. The keys in the `_meta` object are reverse-DNS namespaced, and the values are a dictionary of platform-specific metadata.
- **localization**: Location of translated strings for user-facing fields (`resources` path containing a `${locale}` placeholder and `default_locale`).

### Localization

Provide localized strings without bloating the manifest by pointing to external per-locale resource files. A localization entry looks like this:

```json
"localization": {
"resources": "resources/${locale}.json",
"default_locale": "en-US"
}
```

- `resources` must include a `${locale}` placeholder. Clients resolve it relative to the server install directory.
- `default_locale` must be a valid [BCP 47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) identifier such as `en-US` or `zh-Hans`.
- Values for the default locale stay in the main manifest; localized files only need to contain overrides.
- When a translation is missing, clients fall back to the default locale value from the manifest.

### Icons

Use the `icons` array when you need multiple icon variants (different sizes or themes):

```json
"icons": [
{ "src": "assets/icons/icon-16-light.png", "sizes": "16x16", "theme": "light" },
{ "src": "assets/icons/icon-16-dark.png", "sizes": "16x16", "theme": "dark" }
]
```

- `sizes` must be in `WIDTHxHEIGHT` form (e.g., `128x128`).
- `theme` is optional; use values like `light`, `dark`, or platform-specific labels (e.g., `high-contrast`).
- The legacy `icon` field remains supported for single assets—clients use it when `icons` is omitted.

## Compatibility

Expand Down
120 changes: 118 additions & 2 deletions src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,58 @@ export async function promptVisualAssets() {
},
});

const addIconVariants = await confirm({
message: "Add theme/size-specific icons array?",
default: false,
});

const icons: Array<{
src: string;
sizes: string;
theme?: string;
}> = [];

if (addIconVariants) {
let addMoreIcons = true;
while (addMoreIcons) {
const iconSrc = await input({
message: "Icon source path (relative to manifest):",
validate: (value) => {
if (!value.trim()) return "Icon path is required";
if (value.includes("..")) return "Relative paths cannot include '..'";
return true;
},
});

const iconSizes = await input({
message: "Icon size (e.g., 16x16):",
validate: (value) => {
if (!value.trim()) return "Icon size is required";
if (!/^\d+x\d+$/.test(value)) {
return "Icon size must be in WIDTHxHEIGHT format (e.g., 128x128)";
}
return true;
},
});

const iconTheme = await input({
message: "Icon theme (light, dark, or custom - optional):",
default: "",
});

icons.push({
src: iconSrc,
sizes: iconSizes,
...(iconTheme.trim() ? { theme: iconTheme.trim() } : {}),
});

addMoreIcons = await confirm({
message: "Add another icon entry?",
default: false,
});
}
}

const addScreenshots = await confirm({
message: "Add screenshots?",
default: false,
Expand All @@ -475,7 +527,56 @@ export async function promptVisualAssets() {
}
}

return { icon, screenshots };
return { icon, icons, screenshots };
}

export async function promptLocalization() {
const configureLocalization = await confirm({
message: "Configure localization resources?",
default: false,
});

if (!configureLocalization) {
return undefined;
}

const placeholderRegex = /\$\{locale\}/i;

const resourcesPath = await input({
message: "Localization resources path (must include ${locale} placeholder):",
default: "resources/${locale}.json",
validate: (value) => {
if (!value.trim()) {
return "Resources path is required";
}
if (value.includes("..")) {
return "Relative paths cannot include '..'";
}
if (!placeholderRegex.test(value)) {
return "Path must include a ${locale} placeholder";
}
return true;
},
});

const defaultLocale = await input({
message: "Default locale (BCP 47, e.g., en-US):",
default: "en-US",
validate: (value) => {
if (!value.trim()) {
return "Default locale is required";
}
if (!/^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value)) {
return "Default locale must follow BCP 47 (e.g., en-US or zh-Hans)";
}
return true;
},
});

return {
resources: resourcesPath,
default_locale: defaultLocale,
};
}

export async function promptCompatibility(
Expand Down Expand Up @@ -721,6 +822,11 @@ export function buildManifest(
},
visualAssets: {
icon: string;
icons: Array<{
src: string;
sizes: string;
theme?: string;
}>;
screenshots: string[];
},
serverConfig: {
Expand Down Expand Up @@ -767,6 +873,10 @@ export function buildManifest(
license: string;
repository?: { type: string; url: string };
},
localization?: {
resources: string;
default_locale: string;
},
): McpbManifest {
const { name, displayName, version, description, authorName } = basicInfo;
const { authorEmail, authorUrl } = authorInfo;
Expand All @@ -791,9 +901,11 @@ export function buildManifest(
...(urls.documentation ? { documentation: urls.documentation } : {}),
...(urls.support ? { support: urls.support } : {}),
...(visualAssets.icon ? { icon: visualAssets.icon } : {}),
...(visualAssets.icons.length > 0 ? { icons: visualAssets.icons } : {}),
...(visualAssets.screenshots.length > 0
? { screenshots: visualAssets.screenshots }
: {}),
...(localization ? { localization } : {}),
server: {
type: serverType,
entry_point: entryPoint,
Expand Down Expand Up @@ -876,8 +988,11 @@ export async function initExtension(
? { homepage: "", documentation: "", support: "" }
: await promptUrls();
const visualAssets = nonInteractive
? { icon: "", screenshots: [] }
? { icon: "", icons: [], screenshots: [] }
: await promptVisualAssets();
const localization = nonInteractive
? undefined
: await promptLocalization();
const serverConfig = nonInteractive
? getDefaultServerConfig(packageData)
: await promptServerConfig(packageData);
Expand Down Expand Up @@ -910,6 +1025,7 @@ export async function initExtension(
compatibility,
userConfig,
optionalFields,
localization,
);

// Write manifest
Expand Down
35 changes: 35 additions & 0 deletions src/schemas/0.3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import * as z from "zod";

export const MANIFEST_VERSION = "0.3";

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(),
Expand Down Expand Up @@ -77,6 +81,35 @@ export const McpbUserConfigValuesSchema = z.record(
z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]),
);

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(),
sizes: z
.string()
.regex(
ICON_SIZE_REGEX,
'sizes 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(),
Expand All @@ -96,7 +129,9 @@ export const McpbManifestSchema = z
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(),
Expand Down
2 changes: 1 addition & 1 deletion src/schemas/latest.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./0.2.js";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto re: comment above

export * from "./0.3.js";
Loading