diff --git a/MANIFEST.md b/MANIFEST.md index dffa460..3b38944 100644 --- a/MANIFEST.md +++ b/MANIFEST.md @@ -155,6 +155,54 @@ A full `manifest.json` with most of the optional fields looks like this: "min": 1, "max": 100 } + }, + "_meta": { + "com.microsoft.windows": { + "package_family_name": "MyMcpMSIXPackage_51g09708xawrw", + "static_responses": { + "initialize": { + "capabilities": {}, + "instructions": "When the user wants to search files, use the search_files tool but only after asking them whether the files are local.", + "protocolVersion": "2025-06-18", + "serverInfo": { + "name": "MyMCPExtension", + "title": "My MCP Extension - Pro Edition", + "version": "1.0.0" + } + }, + "tools/list": { + "tools": [ + { + "name": "search_files", + "title": "File search", + "description": "Search for files in a directory", + "inputSchema": { + "type": "object", + "properties": { + "fileSpec": { + "type": "string", + "description": "The file name to search for. Wildcards are supported" + } + }, + "required": ["fileSpec"] + }, + "outputSchema": { + "type": "object", + "properties": { + "searchResults": { + "type": "array", + "description": "The list of file paths that were found", + "items": { + "type": "string" + } + } + }, + } + } + ] + } + }, + } } } ``` @@ -189,6 +237,7 @@ A full `manifest.json` with most of the optional fields looks like this: - **privacy_policies**: Array of URLs to privacy policies for external services that handle user data. Required when the extension connects to external services (first- or third-party) that process user data. Each URL should link to the respective service's privacy policy document - **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. ## Compatibility @@ -606,3 +655,4 @@ The `_generated` fields: - **prompts_generated**: Server generates additional prompts beyond those listed (default: false) This helps implementing apps understand that querying the server at runtime will reveal more capabilities than what's declared in the manifest. + diff --git a/src/schemas-loose.ts b/src/schemas-loose.ts index e44ba2a..7d6ca6d 100644 --- a/src/schemas-loose.ts +++ b/src/schemas-loose.ts @@ -107,6 +107,7 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), + _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), }) .refine((data) => !!(data.dxt_version || data.manifest_version), { message: diff --git a/src/schemas.ts b/src/schemas.ts index 1f7ece2..50777b8 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -100,7 +100,7 @@ export const McpbManifestSchema = z screenshots: z.array(z.string()).optional(), server: McpbManifestServerSchema, tools: z.array(McpbManifestToolSchema).optional(), - tools_generated: z.boolean().optional(), + tools_generated: z.boolean().optional(), prompts: z.array(McpbManifestPromptSchema).optional(), prompts_generated: z.boolean().optional(), keywords: z.array(z.string()).optional(), @@ -110,6 +110,7 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), + _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), }) .refine((data) => !!(data.dxt_version || data.manifest_version), { message: diff --git a/test/schemas.test.ts b/test/schemas.test.ts index a9297d8..86c6ffa 100644 --- a/test/schemas.test.ts +++ b/test/schemas.test.ts @@ -136,4 +136,75 @@ describe("McpbManifestSchema", () => { expect(result.success).toBe(true); }); }); + + describe("_meta", () => { + const base = { + manifest_version: "0.2", + name: "client-ext-test", + 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("accepts valid _meta object with nested dictionaries", () => { + const manifest = { + ...base, + _meta: { + "com.microsoft.windows": { package_family_name: "Pkg_123", channel: "stable" }, + "com.apple.darwin": { bundle_id: "com.example.app", notarized: true }, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(true); + }); + + it("rejects primitive value in _meta entry", () => { + const manifest = { + ...base, + _meta: { + "com.microsoft.windows": "raw-string" as unknown as Record, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(false); + if (!result.success) { + const messages = result.error.issues.map((i) => i.message).join("\n"); + expect(messages).toMatch(/Expected object/); + } + }); + + it("rejects array value in _meta entry", () => { + const manifest = { + ...base, + _meta: { + "com.apple.darwin": [] as unknown as Record, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(false); + }); + + it("rejects null value in _meta entry", () => { + const manifest = { + ...base, + _meta: { + custom: null as unknown as Record, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(false); + }); + + it("allows empty object for _meta", () => { + const manifest = { ...base, _meta: {} }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(true); + }); + }); + });