Skip to content

Commit 1640ade

Browse files
committed
Add localization/theming feature to 0.3 version
1 parent d595dd6 commit 1640ade

File tree

13 files changed

+559
-18
lines changed

13 files changed

+559
-18
lines changed

MANIFEST.md

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# MCPB Manifest.json Spec
22

3-
Current version: `0.2`
4-
Last updated: 2025-09-12
3+
Current version: `0.3`
4+
Last updated: 2025-10-24
55

66
## Manifest Schema
77

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

1212
```jsonc
1313
{
14-
"manifest_version": "0.2", // Manifest spec version this manifest conforms to
14+
"manifest_version": "0.3", // Manifest spec version this manifest conforms to
1515
"name": "my-extension", // Machine-readable name (used for CLI, APIs)
1616
"version": "1.0.0", // Semantic version of your extension
1717
"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:
3737

3838
```json
3939
{
40-
"manifest_version": "0.2",
40+
"manifest_version": "0.3",
4141
"name": "my-extension",
4242
"version": "1.0.0",
4343
"description": "A simple MCP extension",
@@ -71,7 +71,7 @@ A full `manifest.json` with most of the optional fields looks like this:
7171

7272
```json
7373
{
74-
"manifest_version": "0.1",
74+
"manifest_version": "0.3",
7575
"name": "My MCP Extension",
7676
"display_name": "My Awesome MCP Extension",
7777
"version": "1.0.0",
@@ -90,10 +90,26 @@ A full `manifest.json` with most of the optional fields looks like this:
9090
"documentation": "https://docs.example.com/my-extension",
9191
"support": "https://github.com/your-username/my-extension/issues",
9292
"icon": "icon.png",
93+
"icons": [
94+
{
95+
"src": "assets/icons/icon-16-light.png",
96+
"sizes": "16x16",
97+
"theme": "light"
98+
},
99+
{
100+
"src": "assets/icons/icon-16-dark.png",
101+
"sizes": "16x16",
102+
"theme": "dark"
103+
}
104+
],
93105
"screenshots": [
94106
"assets/screenshots/screenshot1.png",
95107
"assets/screenshots/screenshot2.png"
96108
],
109+
"localization": {
110+
"resources": "resources/${locale}.json",
111+
"default_locale": "en-US"
112+
},
97113
"server": {
98114
"type": "node",
99115
"entry_point": "server/index.js",
@@ -221,6 +237,7 @@ A full `manifest.json` with most of the optional fields looks like this:
221237
### Optional Fields
222238

223239
- **icon**: Path to a png icon file, either relative in the package or a https:// url.
240+
- **icons**: Array of icon descriptors (`src`, `sizes`, optional `theme`) for light/dark or size-specific assets.
224241
- **display_name**: Human-friendly name for UI display
225242
- **long_description**: Detailed description for extension stores, markdown
226243
- **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:
238255
- **compatibility**: Compatibility requirements (client app version, platforms, and runtime versions)
239256
- **user_config**: User-configurable options for the extension (see User Configuration section)
240257
- **_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.
258+
- **localization**: Location of translated strings for user-facing fields (`resources` path containing a `${locale}` placeholder and `default_locale`).
259+
260+
### Localization
261+
262+
Provide localized strings without bloating the manifest by pointing to external per-locale resource files. A localization entry looks like this:
263+
264+
```json
265+
"localization": {
266+
"resources": "resources/${locale}.json",
267+
"default_locale": "en-US"
268+
}
269+
```
270+
271+
- `resources` must include a `${locale}` placeholder. Clients resolve it relative to the server install directory.
272+
- `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`.
273+
- Values for the default locale stay in the main manifest; localized files only need to contain overrides.
274+
- When a translation is missing, clients fall back to the default locale value from the manifest.
275+
276+
### Icons
277+
278+
Use the `icons` array when you need multiple icon variants (different sizes or themes):
279+
280+
```json
281+
"icons": [
282+
{ "src": "assets/icons/icon-16-light.png", "sizes": "16x16", "theme": "light" },
283+
{ "src": "assets/icons/icon-16-dark.png", "sizes": "16x16", "theme": "dark" }
284+
]
285+
```
286+
287+
- `sizes` must be in `WIDTHxHEIGHT` form (e.g., `128x128`).
288+
- `theme` is optional; use values like `light`, `dark`, or platform-specific labels (e.g., `high-contrast`).
289+
- The legacy `icon` field remains supported for single assets—clients use it when `icons` is omitted.
241290

242291
## Compatibility
243292

src/cli/init.ts

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,58 @@ export async function promptVisualAssets() {
449449
},
450450
});
451451

452+
const addIconVariants = await confirm({
453+
message: "Add theme/size-specific icons array?",
454+
default: false,
455+
});
456+
457+
const icons: Array<{
458+
src: string;
459+
sizes: string;
460+
theme?: string;
461+
}> = [];
462+
463+
if (addIconVariants) {
464+
let addMoreIcons = true;
465+
while (addMoreIcons) {
466+
const iconSrc = await input({
467+
message: "Icon source path (relative to manifest):",
468+
validate: (value) => {
469+
if (!value.trim()) return "Icon path is required";
470+
if (value.includes("..")) return "Relative paths cannot include '..'";
471+
return true;
472+
},
473+
});
474+
475+
const iconSizes = await input({
476+
message: "Icon size (e.g., 16x16):",
477+
validate: (value) => {
478+
if (!value.trim()) return "Icon size is required";
479+
if (!/^\d+x\d+$/.test(value)) {
480+
return "Icon size must be in WIDTHxHEIGHT format (e.g., 128x128)";
481+
}
482+
return true;
483+
},
484+
});
485+
486+
const iconTheme = await input({
487+
message: "Icon theme (light, dark, or custom - optional):",
488+
default: "",
489+
});
490+
491+
icons.push({
492+
src: iconSrc,
493+
sizes: iconSizes,
494+
...(iconTheme.trim() ? { theme: iconTheme.trim() } : {}),
495+
});
496+
497+
addMoreIcons = await confirm({
498+
message: "Add another icon entry?",
499+
default: false,
500+
});
501+
}
502+
}
503+
452504
const addScreenshots = await confirm({
453505
message: "Add screenshots?",
454506
default: false,
@@ -475,7 +527,56 @@ export async function promptVisualAssets() {
475527
}
476528
}
477529

478-
return { icon, screenshots };
530+
return { icon, icons, screenshots };
531+
}
532+
533+
export async function promptLocalization() {
534+
const configureLocalization = await confirm({
535+
message: "Configure localization resources?",
536+
default: false,
537+
});
538+
539+
if (!configureLocalization) {
540+
return undefined;
541+
}
542+
543+
const placeholderRegex = /\$\{locale\}/i;
544+
545+
const resourcesPath = await input({
546+
message: "Localization resources path (must include ${locale} placeholder):",
547+
default: "resources/${locale}.json",
548+
validate: (value) => {
549+
if (!value.trim()) {
550+
return "Resources path is required";
551+
}
552+
if (value.includes("..")) {
553+
return "Relative paths cannot include '..'";
554+
}
555+
if (!placeholderRegex.test(value)) {
556+
return "Path must include a ${locale} placeholder";
557+
}
558+
return true;
559+
},
560+
});
561+
562+
const defaultLocale = await input({
563+
message: "Default locale (BCP 47, e.g., en-US):",
564+
default: "en-US",
565+
validate: (value) => {
566+
if (!value.trim()) {
567+
return "Default locale is required";
568+
}
569+
if (!/^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value)) {
570+
return "Default locale must follow BCP 47 (e.g., en-US or zh-Hans)";
571+
}
572+
return true;
573+
},
574+
});
575+
576+
return {
577+
resources: resourcesPath,
578+
default_locale: defaultLocale,
579+
};
479580
}
480581

481582
export async function promptCompatibility(
@@ -721,6 +822,11 @@ export function buildManifest(
721822
},
722823
visualAssets: {
723824
icon: string;
825+
icons: Array<{
826+
src: string;
827+
sizes: string;
828+
theme?: string;
829+
}>;
724830
screenshots: string[];
725831
},
726832
serverConfig: {
@@ -767,6 +873,10 @@ export function buildManifest(
767873
license: string;
768874
repository?: { type: string; url: string };
769875
},
876+
localization?: {
877+
resources: string;
878+
default_locale: string;
879+
},
770880
): McpbManifest {
771881
const { name, displayName, version, description, authorName } = basicInfo;
772882
const { authorEmail, authorUrl } = authorInfo;
@@ -791,9 +901,11 @@ export function buildManifest(
791901
...(urls.documentation ? { documentation: urls.documentation } : {}),
792902
...(urls.support ? { support: urls.support } : {}),
793903
...(visualAssets.icon ? { icon: visualAssets.icon } : {}),
904+
...(visualAssets.icons.length > 0 ? { icons: visualAssets.icons } : {}),
794905
...(visualAssets.screenshots.length > 0
795906
? { screenshots: visualAssets.screenshots }
796907
: {}),
908+
...(localization ? { localization } : {}),
797909
server: {
798910
type: serverType,
799911
entry_point: entryPoint,
@@ -876,8 +988,11 @@ export async function initExtension(
876988
? { homepage: "", documentation: "", support: "" }
877989
: await promptUrls();
878990
const visualAssets = nonInteractive
879-
? { icon: "", screenshots: [] }
991+
? { icon: "", icons: [], screenshots: [] }
880992
: await promptVisualAssets();
993+
const localization = nonInteractive
994+
? undefined
995+
: await promptLocalization();
881996
const serverConfig = nonInteractive
882997
? getDefaultServerConfig(packageData)
883998
: await promptServerConfig(packageData);
@@ -910,6 +1025,7 @@ export async function initExtension(
9101025
compatibility,
9111026
userConfig,
9121027
optionalFields,
1028+
localization,
9131029
);
9141030

9151031
// Write manifest

src/schemas/0.3.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import * as z from "zod";
33

44
export const MANIFEST_VERSION = "0.3";
55

6+
const LOCALE_PLACEHOLDER_REGEX = /\$\{locale\}/i;
7+
const BCP47_REGEX = /^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$/;
8+
const ICON_SIZE_REGEX = /^\d+x\d+$/;
9+
610
export const McpServerConfigSchema = z.strictObject({
711
command: z.string(),
812
args: z.array(z.string()).optional(),
@@ -77,6 +81,35 @@ export const McpbUserConfigValuesSchema = z.record(
7781
z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]),
7882
);
7983

84+
export const McpbManifestLocalizationSchema = z.strictObject({
85+
resources: z
86+
.string()
87+
.regex(
88+
LOCALE_PLACEHOLDER_REGEX,
89+
'resources must include a "${locale}" placeholder',
90+
),
91+
default_locale: z
92+
.string()
93+
.regex(
94+
BCP47_REGEX,
95+
"default_locale must be a valid BCP 47 locale identifier",
96+
),
97+
});
98+
99+
export const McpbManifestIconSchema = z.strictObject({
100+
src: z.string(),
101+
sizes: z
102+
.string()
103+
.regex(
104+
ICON_SIZE_REGEX,
105+
'sizes must be in the format "WIDTHxHEIGHT" (e.g., "16x16")',
106+
),
107+
theme: z
108+
.string()
109+
.min(1, "theme cannot be empty when provided")
110+
.optional(),
111+
});
112+
80113
export const McpbManifestSchema = z
81114
.strictObject({
82115
$schema: z.string().optional(),
@@ -96,7 +129,9 @@ export const McpbManifestSchema = z
96129
documentation: z.string().url().optional(),
97130
support: z.string().url().optional(),
98131
icon: z.string().optional(),
132+
icons: z.array(McpbManifestIconSchema).optional(),
99133
screenshots: z.array(z.string()).optional(),
134+
localization: McpbManifestLocalizationSchema.optional(),
100135
server: McpbManifestServerSchema,
101136
tools: z.array(McpbManifestToolSchema).optional(),
102137
tools_generated: z.boolean().optional(),

src/schemas/latest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from "./0.2.js";
1+
export * from "./0.3.js";

0 commit comments

Comments
 (0)