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); + }); + }); }); +