From 90c18ca9fb91d4bb51989193c307de25840f23a6 Mon Sep 17 00:00:00 2001 From: Alexander Sklar Date: Fri, 10 Oct 2025 12:05:18 -0700 Subject: [PATCH 1/3] Add static_responses and client_extensions to manifest schema and tests --- MANIFEST.md | 112 ++++++++++++++++++++++++++++++++ src/schemas-loose.ts | 2 + src/schemas.ts | 4 +- test/schemas.test.ts | 151 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 268 insertions(+), 1 deletion(-) diff --git a/MANIFEST.md b/MANIFEST.md index dffa460..51d187d 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 } + }, + "static_responses": { + "initialize": { + "capabilities": {}, + "instructions": "Consider whether the user doesn't like puppies - if they don't, don't use the search_files tool.", + "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" + } + } + }, + } + } + ] + } + }, + "client_extensions": { + "windows": { + "package_family_name": "MyMcpMSIXPackage_51g09708xawrw" + } } } ``` @@ -189,6 +237,8 @@ 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) +- **static_responses**: A set of static response to protocol messages (for example, `initialize`, `tools/list`, etc.). This allows clients to avoid running a server when a static response is present (e.g. to get the set of tools with their input schemas). For more information, consult the [MCP specification](https://modelcontextprotocol.io/specification). +- **client_extensions**: Platform-specific client integration metadata (e.g., Windows `package_family_name`, macOS bundle identifiers) enabling tighter OS/app store integration ## Compatibility @@ -548,6 +598,8 @@ These fields describe the tools and prompts your MCP server provides. For server Note: Resources are not included in the manifest because MCP resources are inherently dynamic - they represent URIs to data that the server discovers at runtime based on configuration, filesystem state, database connections, etc. +You can provide richer static tools by adding an `tools/list` entry inside the `static_responses` object. + ### Static Declaration For servers with a fixed set of capabilities, list them in arrays. @@ -606,3 +658,63 @@ 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. + +## Static responses + +Servers can provide clients with static responses to protocol messages. Static responses let clients short-circuit server startup for discovery or platform integration metadata. +This is useful to obtain rich tool schema information, server instructions, protocol version that the server supports, etc. +The `static_responses` object is a dictionary where the key is the name of an MCP JSON-RPC method. For more information, consult the [MCP specification](https://modelcontextprotocol.io/specification). + +If both the `tools/list` static response and the `tools` array are present, clients should use the tools described in `tools/list`. + +Example: + +```json +{ + "static_responses": { + "initialize": { + "capabilities": {}, + "instructions": "Consider whether the user doesn't like puppies - if they don't, don't use the search_files tool.", + "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" + } + } + }, + } + } + ] + } + }, +} +``` + + diff --git a/src/schemas-loose.ts b/src/schemas-loose.ts index e44ba2a..7946ffa 100644 --- a/src/schemas-loose.ts +++ b/src/schemas-loose.ts @@ -107,6 +107,8 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), + static_responses: z.record(z.string(), z.any()).optional(), + client_extensions: 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..d3d93f4 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,8 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), + static_responses: z.record(z.string(), z.any()).optional(), + client_extensions: 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..1a3f6bc 100644 --- a/test/schemas.test.ts +++ b/test/schemas.test.ts @@ -136,4 +136,155 @@ describe("McpbManifestSchema", () => { expect(result.success).toBe(true); }); }); + + describe("client_extensions", () => { + 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 client_extensions object with nested dictionaries", () => { + const manifest = { + ...base, + client_extensions: { + windows: { package_family_name: "Pkg_123", channel: "stable" }, + darwin: { bundle_id: "com.example.app", notarized: true }, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(true); + }); + + it("rejects primitive value in client_extensions entry", () => { + const manifest = { + ...base, + client_extensions: { + 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 client_extensions entry", () => { + const manifest = { + ...base, + client_extensions: { + linux: [] as unknown as Record, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(false); + }); + + it("rejects null value in client_extensions entry", () => { + const manifest = { + ...base, + client_extensions: { + custom: null as unknown as Record, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(false); + }); + + it("allows empty object for client_extensions", () => { + const manifest = { ...base, client_extensions: {} }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(true); + }); + }); + + describe("static_responses", () => { + const base = { + manifest_version: "0.2", + name: "static-resp-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 static_responses with object entries", () => { + const manifest = { + ...base, + static_responses: { + initialize: { + capabilities: {}, + protocolVersion: "2025-06-18", + serverInfo: { name: "Test", version: "1.0.0" }, + }, + "tools/list": { + tools: [ + { + name: "search_files", + description: "Search for files", + inputSchema: { type: "object", properties: { query: { type: "string" } } }, + }, + ], + }, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(true); + }); + + it("accepts primitive value in static_responses entry", () => { + const manifest = { + ...base, + static_responses: { + ping: "pong", + code: 200, + ok: true, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(true); + }); + + it("accepts array value in static_responses entry", () => { + const manifest = { + ...base, + static_responses: { + list: [1, 2, 3], + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(true); + }); + + it("accepts null value in static_responses entry", () => { + const manifest = { + ...base, + static_responses: { + initialize: null, + }, + }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(true); + }); + + it("allows empty object for static_responses", () => { + const manifest = { ...base, static_responses: {} }; + const result = McpbManifestSchema.safeParse(manifest); + expect(result.success).toBe(true); + }); + }); }); + From caa61bcc49a4a3f48682fc3498f643ef4bd9b5c8 Mon Sep 17 00:00:00 2001 From: Alexander Sklar Date: Fri, 10 Oct 2025 14:01:49 -0700 Subject: [PATCH 2/3] Update example to be more real --- MANIFEST.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.md b/MANIFEST.md index 51d187d..d4d56be 100644 --- a/MANIFEST.md +++ b/MANIFEST.md @@ -159,7 +159,7 @@ A full `manifest.json` with most of the optional fields looks like this: "static_responses": { "initialize": { "capabilities": {}, - "instructions": "Consider whether the user doesn't like puppies - if they don't, don't use the search_files tool.", + "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", @@ -674,7 +674,7 @@ Example: "static_responses": { "initialize": { "capabilities": {}, - "instructions": "Consider whether the user doesn't like puppies - if they don't, don't use the search_files tool.", + "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", From 443d8494206d0468c6d76476f1a4a2a977530409 Mon Sep 17 00:00:00 2001 From: Alexander Sklar Date: Fri, 10 Oct 2025 16:19:59 -0700 Subject: [PATCH 3/3] Refactor manifest schema to replace static_responses and client_extensions with _meta for improved organization --- MANIFEST.md | 152 +++++++++++++------------------------------ src/schemas-loose.ts | 3 +- src/schemas.ts | 3 +- test/schemas.test.ts | 110 +++++-------------------------- 4 files changed, 62 insertions(+), 206 deletions(-) diff --git a/MANIFEST.md b/MANIFEST.md index d4d56be..3b38944 100644 --- a/MANIFEST.md +++ b/MANIFEST.md @@ -156,52 +156,52 @@ A full `manifest.json` with most of the optional fields looks like this: "max": 100 } }, - "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" - } - } - }, + "_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" + } + } + }, + } + } + ] } - ] - } - }, - "client_extensions": { - "windows": { - "package_family_name": "MyMcpMSIXPackage_51g09708xawrw" + }, } } } @@ -237,8 +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) -- **static_responses**: A set of static response to protocol messages (for example, `initialize`, `tools/list`, etc.). This allows clients to avoid running a server when a static response is present (e.g. to get the set of tools with their input schemas). For more information, consult the [MCP specification](https://modelcontextprotocol.io/specification). -- **client_extensions**: Platform-specific client integration metadata (e.g., Windows `package_family_name`, macOS bundle identifiers) enabling tighter OS/app store integration +- **_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 @@ -598,8 +597,6 @@ These fields describe the tools and prompts your MCP server provides. For server Note: Resources are not included in the manifest because MCP resources are inherently dynamic - they represent URIs to data that the server discovers at runtime based on configuration, filesystem state, database connections, etc. -You can provide richer static tools by adding an `tools/list` entry inside the `static_responses` object. - ### Static Declaration For servers with a fixed set of capabilities, list them in arrays. @@ -659,62 +656,3 @@ The `_generated` fields: This helps implementing apps understand that querying the server at runtime will reveal more capabilities than what's declared in the manifest. -## Static responses - -Servers can provide clients with static responses to protocol messages. Static responses let clients short-circuit server startup for discovery or platform integration metadata. -This is useful to obtain rich tool schema information, server instructions, protocol version that the server supports, etc. -The `static_responses` object is a dictionary where the key is the name of an MCP JSON-RPC method. For more information, consult the [MCP specification](https://modelcontextprotocol.io/specification). - -If both the `tools/list` static response and the `tools` array are present, clients should use the tools described in `tools/list`. - -Example: - -```json -{ - "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" - } - } - }, - } - } - ] - } - }, -} -``` - - diff --git a/src/schemas-loose.ts b/src/schemas-loose.ts index 7946ffa..7d6ca6d 100644 --- a/src/schemas-loose.ts +++ b/src/schemas-loose.ts @@ -107,8 +107,7 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), - static_responses: z.record(z.string(), z.any()).optional(), - client_extensions: z.record(z.string(), z.record(z.string(), z.any())).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 d3d93f4..50777b8 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -110,8 +110,7 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), - static_responses: z.record(z.string(), z.any()).optional(), - client_extensions: z.record(z.string(), z.record(z.string(), z.any())).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 1a3f6bc..86c6ffa 100644 --- a/test/schemas.test.ts +++ b/test/schemas.test.ts @@ -137,7 +137,7 @@ describe("McpbManifestSchema", () => { }); }); - describe("client_extensions", () => { + describe("_meta", () => { const base = { manifest_version: "0.2", name: "client-ext-test", @@ -151,23 +151,23 @@ describe("McpbManifestSchema", () => { }, }; - it("accepts valid client_extensions object with nested dictionaries", () => { + it("accepts valid _meta object with nested dictionaries", () => { const manifest = { ...base, - client_extensions: { - windows: { package_family_name: "Pkg_123", channel: "stable" }, - darwin: { bundle_id: "com.example.app", notarized: true }, + _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 client_extensions entry", () => { + it("rejects primitive value in _meta entry", () => { const manifest = { ...base, - client_extensions: { - windows: "raw-string" as unknown as Record, + _meta: { + "com.microsoft.windows": "raw-string" as unknown as Record, }, }; const result = McpbManifestSchema.safeParse(manifest); @@ -178,21 +178,21 @@ describe("McpbManifestSchema", () => { } }); - it("rejects array value in client_extensions entry", () => { + it("rejects array value in _meta entry", () => { const manifest = { ...base, - client_extensions: { - linux: [] as unknown as Record, + _meta: { + "com.apple.darwin": [] as unknown as Record, }, }; const result = McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(false); }); - it("rejects null value in client_extensions entry", () => { + it("rejects null value in _meta entry", () => { const manifest = { ...base, - client_extensions: { + _meta: { custom: null as unknown as Record, }, }; @@ -200,91 +200,11 @@ describe("McpbManifestSchema", () => { expect(result.success).toBe(false); }); - it("allows empty object for client_extensions", () => { - const manifest = { ...base, client_extensions: {} }; + it("allows empty object for _meta", () => { + const manifest = { ...base, _meta: {} }; const result = McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(true); }); }); - describe("static_responses", () => { - const base = { - manifest_version: "0.2", - name: "static-resp-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 static_responses with object entries", () => { - const manifest = { - ...base, - static_responses: { - initialize: { - capabilities: {}, - protocolVersion: "2025-06-18", - serverInfo: { name: "Test", version: "1.0.0" }, - }, - "tools/list": { - tools: [ - { - name: "search_files", - description: "Search for files", - inputSchema: { type: "object", properties: { query: { type: "string" } } }, - }, - ], - }, - }, - }; - const result = McpbManifestSchema.safeParse(manifest); - expect(result.success).toBe(true); - }); - - it("accepts primitive value in static_responses entry", () => { - const manifest = { - ...base, - static_responses: { - ping: "pong", - code: 200, - ok: true, - }, - }; - const result = McpbManifestSchema.safeParse(manifest); - expect(result.success).toBe(true); - }); - - it("accepts array value in static_responses entry", () => { - const manifest = { - ...base, - static_responses: { - list: [1, 2, 3], - }, - }; - const result = McpbManifestSchema.safeParse(manifest); - expect(result.success).toBe(true); - }); - - it("accepts null value in static_responses entry", () => { - const manifest = { - ...base, - static_responses: { - initialize: null, - }, - }; - const result = McpbManifestSchema.safeParse(manifest); - expect(result.success).toBe(true); - }); - - it("allows empty object for static_responses", () => { - const manifest = { ...base, static_responses: {} }; - const result = McpbManifestSchema.safeParse(manifest); - expect(result.success).toBe(true); - }); - }); }); -