Add localization/theming feature to 0.3 version
This commit is contained in:
parent
d595dd66dc
commit
1640adefe8
13 changed files with 559 additions and 18 deletions
59
MANIFEST.md
59
MANIFEST.md
|
|
@ -1,7 +1,7 @@
|
||||||
# MCPB Manifest.json Spec
|
# MCPB Manifest.json Spec
|
||||||
|
|
||||||
Current version: `0.2`
|
Current version: `0.3`
|
||||||
Last updated: 2025-09-12
|
Last updated: 2025-10-24
|
||||||
|
|
||||||
## Manifest Schema
|
## Manifest Schema
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ A basic `manifest.json` with just the required fields looks like this:
|
||||||
|
|
||||||
```jsonc
|
```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)
|
"name": "my-extension", // Machine-readable name (used for CLI, APIs)
|
||||||
"version": "1.0.0", // Semantic version of your extension
|
"version": "1.0.0", // Semantic version of your extension
|
||||||
"description": "A simple MCP extension", // Brief description of what the extension does
|
"description": "A simple MCP extension", // Brief description of what the extension does
|
||||||
|
|
@ -37,7 +37,7 @@ A basic `manifest.json` with just the required fields looks like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"manifest_version": "0.2",
|
"manifest_version": "0.3",
|
||||||
"name": "my-extension",
|
"name": "my-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A simple MCP extension",
|
"description": "A simple MCP extension",
|
||||||
|
|
@ -71,7 +71,7 @@ A full `manifest.json` with most of the optional fields looks like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"manifest_version": "0.1",
|
"manifest_version": "0.3",
|
||||||
"name": "My MCP Extension",
|
"name": "My MCP Extension",
|
||||||
"display_name": "My Awesome MCP Extension",
|
"display_name": "My Awesome MCP Extension",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
@ -90,10 +90,26 @@ A full `manifest.json` with most of the optional fields looks like this:
|
||||||
"documentation": "https://docs.example.com/my-extension",
|
"documentation": "https://docs.example.com/my-extension",
|
||||||
"support": "https://github.com/your-username/my-extension/issues",
|
"support": "https://github.com/your-username/my-extension/issues",
|
||||||
"icon": "icon.png",
|
"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": [
|
"screenshots": [
|
||||||
"assets/screenshots/screenshot1.png",
|
"assets/screenshots/screenshot1.png",
|
||||||
"assets/screenshots/screenshot2.png"
|
"assets/screenshots/screenshot2.png"
|
||||||
],
|
],
|
||||||
|
"localization": {
|
||||||
|
"resources": "resources/${locale}.json",
|
||||||
|
"default_locale": "en-US"
|
||||||
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"entry_point": "server/index.js",
|
"entry_point": "server/index.js",
|
||||||
|
|
@ -221,6 +237,7 @@ A full `manifest.json` with most of the optional fields looks like this:
|
||||||
### Optional Fields
|
### Optional Fields
|
||||||
|
|
||||||
- **icon**: Path to a png icon file, either relative in the package or a https:// url.
|
- **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
|
- **display_name**: Human-friendly name for UI display
|
||||||
- **long_description**: Detailed description for extension stores, markdown
|
- **long_description**: Detailed description for extension stores, markdown
|
||||||
- **repository**: Source code repository information (type and url)
|
- **repository**: Source code repository information (type and url)
|
||||||
|
|
@ -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)
|
- **compatibility**: Compatibility requirements (client app version, platforms, and runtime versions)
|
||||||
- **user_config**: User-configurable options for the extension (see User Configuration section)
|
- **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.
|
- **_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
|
## Compatibility
|
||||||
|
|
||||||
|
|
|
||||||
120
src/cli/init.ts
120
src/cli/init.ts
|
|
@ -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({
|
const addScreenshots = await confirm({
|
||||||
message: "Add screenshots?",
|
message: "Add screenshots?",
|
||||||
default: false,
|
default: false,
|
||||||
|
|
@ -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(
|
export async function promptCompatibility(
|
||||||
|
|
@ -721,6 +822,11 @@ export function buildManifest(
|
||||||
},
|
},
|
||||||
visualAssets: {
|
visualAssets: {
|
||||||
icon: string;
|
icon: string;
|
||||||
|
icons: Array<{
|
||||||
|
src: string;
|
||||||
|
sizes: string;
|
||||||
|
theme?: string;
|
||||||
|
}>;
|
||||||
screenshots: string[];
|
screenshots: string[];
|
||||||
},
|
},
|
||||||
serverConfig: {
|
serverConfig: {
|
||||||
|
|
@ -767,6 +873,10 @@ export function buildManifest(
|
||||||
license: string;
|
license: string;
|
||||||
repository?: { type: string; url: string };
|
repository?: { type: string; url: string };
|
||||||
},
|
},
|
||||||
|
localization?: {
|
||||||
|
resources: string;
|
||||||
|
default_locale: string;
|
||||||
|
},
|
||||||
): McpbManifest {
|
): McpbManifest {
|
||||||
const { name, displayName, version, description, authorName } = basicInfo;
|
const { name, displayName, version, description, authorName } = basicInfo;
|
||||||
const { authorEmail, authorUrl } = authorInfo;
|
const { authorEmail, authorUrl } = authorInfo;
|
||||||
|
|
@ -791,9 +901,11 @@ export function buildManifest(
|
||||||
...(urls.documentation ? { documentation: urls.documentation } : {}),
|
...(urls.documentation ? { documentation: urls.documentation } : {}),
|
||||||
...(urls.support ? { support: urls.support } : {}),
|
...(urls.support ? { support: urls.support } : {}),
|
||||||
...(visualAssets.icon ? { icon: visualAssets.icon } : {}),
|
...(visualAssets.icon ? { icon: visualAssets.icon } : {}),
|
||||||
|
...(visualAssets.icons.length > 0 ? { icons: visualAssets.icons } : {}),
|
||||||
...(visualAssets.screenshots.length > 0
|
...(visualAssets.screenshots.length > 0
|
||||||
? { screenshots: visualAssets.screenshots }
|
? { screenshots: visualAssets.screenshots }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(localization ? { localization } : {}),
|
||||||
server: {
|
server: {
|
||||||
type: serverType,
|
type: serverType,
|
||||||
entry_point: entryPoint,
|
entry_point: entryPoint,
|
||||||
|
|
@ -876,8 +988,11 @@ export async function initExtension(
|
||||||
? { homepage: "", documentation: "", support: "" }
|
? { homepage: "", documentation: "", support: "" }
|
||||||
: await promptUrls();
|
: await promptUrls();
|
||||||
const visualAssets = nonInteractive
|
const visualAssets = nonInteractive
|
||||||
? { icon: "", screenshots: [] }
|
? { icon: "", icons: [], screenshots: [] }
|
||||||
: await promptVisualAssets();
|
: await promptVisualAssets();
|
||||||
|
const localization = nonInteractive
|
||||||
|
? undefined
|
||||||
|
: await promptLocalization();
|
||||||
const serverConfig = nonInteractive
|
const serverConfig = nonInteractive
|
||||||
? getDefaultServerConfig(packageData)
|
? getDefaultServerConfig(packageData)
|
||||||
: await promptServerConfig(packageData);
|
: await promptServerConfig(packageData);
|
||||||
|
|
@ -910,6 +1025,7 @@ export async function initExtension(
|
||||||
compatibility,
|
compatibility,
|
||||||
userConfig,
|
userConfig,
|
||||||
optionalFields,
|
optionalFields,
|
||||||
|
localization,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Write manifest
|
// Write manifest
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import * as z from "zod";
|
||||||
|
|
||||||
export const MANIFEST_VERSION = "0.3";
|
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({
|
export const McpServerConfigSchema = z.strictObject({
|
||||||
command: z.string(),
|
command: z.string(),
|
||||||
args: z.array(z.string()).optional(),
|
args: z.array(z.string()).optional(),
|
||||||
|
|
@ -77,6 +81,35 @@ export const McpbUserConfigValuesSchema = z.record(
|
||||||
z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]),
|
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
|
export const McpbManifestSchema = z
|
||||||
.strictObject({
|
.strictObject({
|
||||||
$schema: z.string().optional(),
|
$schema: z.string().optional(),
|
||||||
|
|
@ -96,7 +129,9 @@ export const McpbManifestSchema = z
|
||||||
documentation: z.string().url().optional(),
|
documentation: z.string().url().optional(),
|
||||||
support: z.string().url().optional(),
|
support: z.string().url().optional(),
|
||||||
icon: z.string().optional(),
|
icon: z.string().optional(),
|
||||||
|
icons: z.array(McpbManifestIconSchema).optional(),
|
||||||
screenshots: z.array(z.string()).optional(),
|
screenshots: z.array(z.string()).optional(),
|
||||||
|
localization: McpbManifestLocalizationSchema.optional(),
|
||||||
server: McpbManifestServerSchema,
|
server: McpbManifestServerSchema,
|
||||||
tools: z.array(McpbManifestToolSchema).optional(),
|
tools: z.array(McpbManifestToolSchema).optional(),
|
||||||
tools_generated: z.boolean().optional(),
|
tools_generated: z.boolean().optional(),
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from "./0.2.js";
|
export * from "./0.3.js";
|
||||||
|
|
|
||||||
164
src/schemas_loose/0.3.ts
Normal file
164
src/schemas_loose/0.3.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
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.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"]),
|
||||||
|
entry_point: z.string(),
|
||||||
|
mcp_config: McpbManifestMcpConfigSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 McpbUserConfigValuesSchema = z.record(
|
||||||
|
z.string(),
|
||||||
|
z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]),
|
||||||
|
);
|
||||||
|
|
||||||
|
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(),
|
||||||
|
sizes: z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
ICON_SIZE_REGEX,
|
||||||
|
'sizes 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",
|
||||||
|
});
|
||||||
|
|
||||||
|
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(),
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * as v0_1 from "./0.1.js";
|
export * as v0_1 from "./0.1.js";
|
||||||
export * as v0_2 from "./0.2.js";
|
export * as v0_2 from "./0.2.js";
|
||||||
|
export * as v0_3 from "./0.3.js";
|
||||||
export * as latest from "./latest.js";
|
export * as latest from "./latest.js";
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from "./0.2.js";
|
export * from "./0.3.js";
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { McpbManifestSchema as ManifestSchemaV0_1 } from "../schemas/0.1.js";
|
import { McpbManifestSchema as ManifestSchemaV0_1 } from "../schemas/0.1.js";
|
||||||
import { McpbManifestSchema as ManifestSchemaV0_2 } from "../schemas/0.2.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 CurrentManifestSchema } from "../schemas/latest.js";
|
||||||
import { McpbManifestSchema as LooseManifestSchemaV0_1 } from "../schemas_loose/0.1.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_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";
|
import { McpbManifestSchema as CurrentLooseManifestSchema } from "../schemas_loose/latest.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Latest manifest version - the version that new manifests should use
|
* Latest manifest version - the version that new manifests should use
|
||||||
*/
|
*/
|
||||||
export const LATEST_MANIFEST_VERSION = "0.2" as const;
|
export const LATEST_MANIFEST_VERSION = "0.3" as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of manifest versions to their strict schemas
|
* Map of manifest versions to their strict schemas
|
||||||
|
|
@ -16,6 +18,7 @@ export const LATEST_MANIFEST_VERSION = "0.2" as const;
|
||||||
export const MANIFEST_SCHEMAS = {
|
export const MANIFEST_SCHEMAS = {
|
||||||
"0.1": ManifestSchemaV0_1,
|
"0.1": ManifestSchemaV0_1,
|
||||||
"0.2": ManifestSchemaV0_2,
|
"0.2": ManifestSchemaV0_2,
|
||||||
|
"0.3": ManifestSchemaV0_3,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -24,6 +27,7 @@ export const MANIFEST_SCHEMAS = {
|
||||||
export const MANIFEST_SCHEMAS_LOOSE = {
|
export const MANIFEST_SCHEMAS_LOOSE = {
|
||||||
"0.1": LooseManifestSchemaV0_1,
|
"0.1": LooseManifestSchemaV0_1,
|
||||||
"0.2": LooseManifestSchemaV0_2,
|
"0.2": LooseManifestSchemaV0_2,
|
||||||
|
"0.3": LooseManifestSchemaV0_3,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ describe("getMcpConfigForManifest", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseManifest: McpbManifest = {
|
const baseManifest: McpbManifest = {
|
||||||
manifest_version: "0.2",
|
manifest_version: "0.3",
|
||||||
name: "test-extension",
|
name: "test-extension",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "Test extension",
|
description: "Test extension",
|
||||||
|
|
@ -305,7 +305,7 @@ describe("getMcpConfigForManifest", () => {
|
||||||
|
|
||||||
describe("hasRequiredConfigMissing", () => {
|
describe("hasRequiredConfigMissing", () => {
|
||||||
const baseManifest: McpbManifest = {
|
const baseManifest: McpbManifest = {
|
||||||
manifest_version: "0.2",
|
manifest_version: "0.3",
|
||||||
name: "test-extension",
|
name: "test-extension",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "Test extension",
|
description: "Test extension",
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,7 @@ describe("init functions", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "",
|
icon: "",
|
||||||
|
icons: [],
|
||||||
screenshots: [],
|
screenshots: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -215,6 +216,7 @@ describe("init functions", () => {
|
||||||
keywords: "",
|
keywords: "",
|
||||||
license: "",
|
license: "",
|
||||||
},
|
},
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(manifest).toEqual({
|
expect(manifest).toEqual({
|
||||||
|
|
@ -258,6 +260,10 @@ describe("init functions", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "icon.png",
|
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: ["screen1.png", "screen2.png"],
|
screenshots: ["screen1.png", "screen2.png"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -301,6 +307,10 @@ describe("init functions", () => {
|
||||||
license: "MIT",
|
license: "MIT",
|
||||||
repository: { type: "git", url: "https://github.com/user/repo" },
|
repository: { type: "git", url: "https://github.com/user/repo" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
resources: "resources/${locale}.json",
|
||||||
|
default_locale: "en-US",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(manifest).toEqual({
|
expect(manifest).toEqual({
|
||||||
|
|
@ -320,7 +330,23 @@ describe("init functions", () => {
|
||||||
documentation: "https://docs.example.com",
|
documentation: "https://docs.example.com",
|
||||||
support: "https://support.example.com",
|
support: "https://support.example.com",
|
||||||
icon: "icon.png",
|
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: ["screen1.png", "screen2.png"],
|
screenshots: ["screen1.png", "screen2.png"],
|
||||||
|
localization: {
|
||||||
|
resources: "resources/${locale}.json",
|
||||||
|
default_locale: "en-US",
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
type: "python",
|
type: "python",
|
||||||
entry_point: "server/main.py",
|
entry_point: "server/main.py",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { McpbManifestSchema, v0_3 } from "../src/schemas/index.js";
|
import { McpbManifestSchema, v0_2, v0_3 } from "../src/schemas/index.js";
|
||||||
|
|
||||||
describe("McpbManifestSchema", () => {
|
describe("McpbManifestSchema", () => {
|
||||||
it("should validate a valid manifest", () => {
|
it("should validate a valid manifest", () => {
|
||||||
|
|
@ -35,9 +35,26 @@ describe("McpbManifestSchema", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should accept a 0.2 manifest when upgraded to 0.3", () => {
|
||||||
|
const manifestPath = join(__dirname, "valid-manifest-0.2.json");
|
||||||
|
const manifestContent = readFileSync(manifestPath, "utf-8");
|
||||||
|
const manifestData = JSON.parse(manifestContent);
|
||||||
|
|
||||||
|
const legacyResult = v0_2.McpbManifestSchema.safeParse(manifestData);
|
||||||
|
expect(legacyResult.success).toBe(true);
|
||||||
|
|
||||||
|
const upgradedManifest = {
|
||||||
|
...manifestData,
|
||||||
|
manifest_version: "0.3",
|
||||||
|
};
|
||||||
|
const upgradedResult = v0_3.McpbManifestSchema.safeParse(upgradedManifest);
|
||||||
|
|
||||||
|
expect(upgradedResult.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("should validate manifest with all optional fields", () => {
|
it("should validate manifest with all optional fields", () => {
|
||||||
const fullManifest = {
|
const fullManifest = {
|
||||||
manifest_version: "0.2",
|
manifest_version: "0.3",
|
||||||
name: "full-extension",
|
name: "full-extension",
|
||||||
display_name: "Full Featured Extension",
|
display_name: "Full Featured Extension",
|
||||||
version: "2.0.0",
|
version: "2.0.0",
|
||||||
|
|
@ -56,7 +73,23 @@ describe("McpbManifestSchema", () => {
|
||||||
documentation: "https://docs.example.com",
|
documentation: "https://docs.example.com",
|
||||||
support: "https://support.example.com",
|
support: "https://support.example.com",
|
||||||
icon: "icon.png",
|
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: ["screenshot1.png", "screenshot2.png"],
|
screenshots: ["screenshot1.png", "screenshot2.png"],
|
||||||
|
localization: {
|
||||||
|
resources: "resources/${locale}.json",
|
||||||
|
default_locale: "en-US",
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
type: "python",
|
type: "python",
|
||||||
entry_point: "main.py",
|
entry_point: "main.py",
|
||||||
|
|
@ -109,6 +142,8 @@ describe("McpbManifestSchema", () => {
|
||||||
expect(result.data.tools).toHaveLength(1);
|
expect(result.data.tools).toHaveLength(1);
|
||||||
expect(result.data.compatibility?.platforms).toContain("darwin");
|
expect(result.data.compatibility?.platforms).toContain("darwin");
|
||||||
expect(result.data.user_config?.api_key.type).toBe("string");
|
expect(result.data.user_config?.api_key.type).toBe("string");
|
||||||
|
expect(result.data.icons).toHaveLength(2);
|
||||||
|
expect(result.data.localization?.default_locale).toBe("en-US");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -117,7 +152,7 @@ describe("McpbManifestSchema", () => {
|
||||||
|
|
||||||
serverTypes.forEach((type) => {
|
serverTypes.forEach((type) => {
|
||||||
const manifest = {
|
const manifest = {
|
||||||
manifest_version: "0.2",
|
manifest_version: "0.3",
|
||||||
name: "test",
|
name: "test",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "Test",
|
description: "Test",
|
||||||
|
|
@ -213,4 +248,92 @@ describe("McpbManifestSchema", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("localization", () => {
|
||||||
|
const base = {
|
||||||
|
manifest_version: "0.3" as const,
|
||||||
|
name: "loc-ext",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "Test manifest",
|
||||||
|
author: { name: "Author" },
|
||||||
|
server: {
|
||||||
|
type: "node" as const,
|
||||||
|
entry_point: "server/index.js",
|
||||||
|
mcp_config: { command: "node", args: ["server/index.js"] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("requires a ${locale} placeholder", () => {
|
||||||
|
const manifest = {
|
||||||
|
...base,
|
||||||
|
localization: {
|
||||||
|
resources: "resources/fr.json",
|
||||||
|
default_locale: "en-US",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = McpbManifestSchema.safeParse(manifest);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
if (!result.success) {
|
||||||
|
const messages = result.error.issues.map((issue) => issue.message);
|
||||||
|
expect(messages.join(" ")).toContain("${locale}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid default locale", () => {
|
||||||
|
const manifest = {
|
||||||
|
...base,
|
||||||
|
localization: {
|
||||||
|
resources: "resources/${locale}.json",
|
||||||
|
default_locale: "en_us",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = McpbManifestSchema.safeParse(manifest);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts valid localization settings", () => {
|
||||||
|
const manifest = {
|
||||||
|
...base,
|
||||||
|
localization: {
|
||||||
|
resources: "resources/${locale}.json",
|
||||||
|
default_locale: "en-US",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = McpbManifestSchema.safeParse(manifest);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("icons", () => {
|
||||||
|
const base = {
|
||||||
|
manifest_version: "0.3" as const,
|
||||||
|
name: "icon-ext",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "Test manifest",
|
||||||
|
author: { name: "Author" },
|
||||||
|
server: {
|
||||||
|
type: "python" as const,
|
||||||
|
entry_point: "main.py",
|
||||||
|
mcp_config: { command: "python", args: ["main.py"] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("rejects icons with invalid size format", () => {
|
||||||
|
const manifest = {
|
||||||
|
...base,
|
||||||
|
icons: [{ src: "assets/icon.png", sizes: "16", theme: "light" }],
|
||||||
|
};
|
||||||
|
const result = McpbManifestSchema.safeParse(manifest);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows icons without theme", () => {
|
||||||
|
const manifest = {
|
||||||
|
...base,
|
||||||
|
icons: [{ src: "assets/icon.png", sizes: "128x128" }],
|
||||||
|
};
|
||||||
|
const result = McpbManifestSchema.safeParse(manifest);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
21
test/valid-manifest-0.2.json
Normal file
21
test/valid-manifest-0.2.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"manifest_version": "0.2",
|
||||||
|
"name": "legacy-extension",
|
||||||
|
"display_name": "Legacy Extension",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "An extension that predates 0.3",
|
||||||
|
"author": {
|
||||||
|
"name": "Legacy Author",
|
||||||
|
"email": "legacy@example.com"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"type": "node",
|
||||||
|
"entry_point": "server/index.js",
|
||||||
|
"mcp_config": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"${__dirname}/server/index.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"manifest_version": "0.2",
|
"manifest_version": "0.3",
|
||||||
"name": "test-extension",
|
"name": "test-extension",
|
||||||
"display_name": "Test Extension",
|
"display_name": "Test Extension",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
@ -13,7 +13,9 @@
|
||||||
"entry_point": "server/index.js",
|
"entry_point": "server/index.js",
|
||||||
"mcp_config": {
|
"mcp_config": {
|
||||||
"command": "node",
|
"command": "node",
|
||||||
"args": ["${__dirname}/server/index.js"]
|
"args": [
|
||||||
|
"${__dirname}/server/index.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue