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
|
||||
|
||||
Current version: `0.2`
|
||||
Last updated: 2025-09-12
|
||||
Current version: `0.3`
|
||||
Last updated: 2025-10-24
|
||||
|
||||
## Manifest Schema
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
@ -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",
|
||||
|
|
@ -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",
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
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({
|
||||
message: "Add screenshots?",
|
||||
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(
|
||||
|
|
@ -721,6 +822,11 @@ export function buildManifest(
|
|||
},
|
||||
visualAssets: {
|
||||
icon: string;
|
||||
icons: Array<{
|
||||
src: string;
|
||||
sizes: string;
|
||||
theme?: string;
|
||||
}>;
|
||||
screenshots: string[];
|
||||
},
|
||||
serverConfig: {
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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);
|
||||
|
|
@ -910,6 +1025,7 @@ export async function initExtension(
|
|||
compatibility,
|
||||
userConfig,
|
||||
optionalFields,
|
||||
localization,
|
||||
);
|
||||
|
||||
// Write manifest
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
@ -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(),
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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_2 from "./0.2.js";
|
||||
export * as v0_3 from "./0.3.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_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
|
||||
*/
|
||||
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
|
||||
|
|
@ -16,6 +18,7 @@ export const LATEST_MANIFEST_VERSION = "0.2" as const;
|
|||
export const MANIFEST_SCHEMAS = {
|
||||
"0.1": ManifestSchemaV0_1,
|
||||
"0.2": ManifestSchemaV0_2,
|
||||
"0.3": ManifestSchemaV0_3,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
|
@ -24,6 +27,7 @@ export const MANIFEST_SCHEMAS = {
|
|||
export const MANIFEST_SCHEMAS_LOOSE = {
|
||||
"0.1": LooseManifestSchemaV0_1,
|
||||
"0.2": LooseManifestSchemaV0_2,
|
||||
"0.3": LooseManifestSchemaV0_3,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ describe("getMcpConfigForManifest", () => {
|
|||
};
|
||||
|
||||
const baseManifest: McpbManifest = {
|
||||
manifest_version: "0.2",
|
||||
manifest_version: "0.3",
|
||||
name: "test-extension",
|
||||
version: "1.0.0",
|
||||
description: "Test extension",
|
||||
|
|
@ -305,7 +305,7 @@ describe("getMcpConfigForManifest", () => {
|
|||
|
||||
describe("hasRequiredConfigMissing", () => {
|
||||
const baseManifest: McpbManifest = {
|
||||
manifest_version: "0.2",
|
||||
manifest_version: "0.3",
|
||||
name: "test-extension",
|
||||
version: "1.0.0",
|
||||
description: "Test extension",
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ describe("init functions", () => {
|
|||
},
|
||||
{
|
||||
icon: "",
|
||||
icons: [],
|
||||
screenshots: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -215,6 +216,7 @@ describe("init functions", () => {
|
|||
keywords: "",
|
||||
license: "",
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(manifest).toEqual({
|
||||
|
|
@ -258,6 +260,10 @@ describe("init functions", () => {
|
|||
},
|
||||
{
|
||||
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"],
|
||||
},
|
||||
{
|
||||
|
|
@ -301,6 +307,10 @@ describe("init functions", () => {
|
|||
license: "MIT",
|
||||
repository: { type: "git", url: "https://github.com/user/repo" },
|
||||
},
|
||||
{
|
||||
resources: "resources/${locale}.json",
|
||||
default_locale: "en-US",
|
||||
},
|
||||
);
|
||||
|
||||
expect(manifest).toEqual({
|
||||
|
|
@ -320,7 +330,23 @@ describe("init functions", () => {
|
|||
documentation: "https://docs.example.com",
|
||||
support: "https://support.example.com",
|
||||
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"],
|
||||
localization: {
|
||||
resources: "resources/${locale}.json",
|
||||
default_locale: "en-US",
|
||||
},
|
||||
server: {
|
||||
type: "python",
|
||||
entry_point: "server/main.py",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { readFileSync } from "fs";
|
||||
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", () => {
|
||||
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", () => {
|
||||
const fullManifest = {
|
||||
manifest_version: "0.2",
|
||||
manifest_version: "0.3",
|
||||
name: "full-extension",
|
||||
display_name: "Full Featured Extension",
|
||||
version: "2.0.0",
|
||||
|
|
@ -56,7 +73,23 @@ describe("McpbManifestSchema", () => {
|
|||
documentation: "https://docs.example.com",
|
||||
support: "https://support.example.com",
|
||||
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"],
|
||||
localization: {
|
||||
resources: "resources/${locale}.json",
|
||||
default_locale: "en-US",
|
||||
},
|
||||
server: {
|
||||
type: "python",
|
||||
entry_point: "main.py",
|
||||
|
|
@ -109,6 +142,8 @@ describe("McpbManifestSchema", () => {
|
|||
expect(result.data.tools).toHaveLength(1);
|
||||
expect(result.data.compatibility?.platforms).toContain("darwin");
|
||||
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) => {
|
||||
const manifest = {
|
||||
manifest_version: "0.2",
|
||||
manifest_version: "0.3",
|
||||
name: "test",
|
||||
version: "1.0.0",
|
||||
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",
|
||||
"display_name": "Test Extension",
|
||||
"version": "1.0.0",
|
||||
|
|
@ -13,7 +13,9 @@
|
|||
"entry_point": "server/index.js",
|
||||
"mcp_config": {
|
||||
"command": "node",
|
||||
"args": ["${__dirname}/server/index.js"]
|
||||
"args": [
|
||||
"${__dirname}/server/index.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue