diff --git a/.changeset/mcp-explicit-hints.md b/.changeset/mcp-explicit-hints.md new file mode 100644 index 0000000..7f7c3b1 --- /dev/null +++ b/.changeset/mcp-explicit-hints.md @@ -0,0 +1,9 @@ +--- +"@codelitdev/mcp-server-kit": minor +--- + +Every tool now advertises all four annotation hints as explicit booleans: `readOnlyHint` and `destructiveHint` from its `risk`, and `idempotentHint` and `openWorldHint` from `annotations`, defaulting to `false`. The ChatGPT Plugins Directory requires `readOnlyHint`, `destructiveHint` and `openWorldHint` to be set explicitly on every tool. Read tools used to leave `destructiveHint` out, and tools without `annotations` left out the other two. + +### Product changes + +A tool that reaches outside the tenant's own workspace (sends email, posts to arbitrary destinations, searches the public web) should now pass `annotations: { openWorldHint: true }`; without it the kit advertises `false`. diff --git a/packages/mcp-server-kit/src/create.test.ts b/packages/mcp-server-kit/src/create.test.ts index 83c57a9..a3ff517 100644 --- a/packages/mcp-server-kit/src/create.test.ts +++ b/packages/mcp-server-kit/src/create.test.ts @@ -162,12 +162,22 @@ describe("createMcpServerKit", () => { expect(listed.ttlMs).toBe(300_000); expect(listed.cacheScope).toBe("private"); const [list, create, remove] = listed.tools; - expect(list?.annotations).toEqual({ readOnlyHint: true }); + const defaults = { idempotentHint: false, openWorldHint: false }; + expect(list?.annotations).toEqual({ + ...defaults, + readOnlyHint: true, + destructiveHint: false, + }); expect(create?.annotations).toEqual({ + ...defaults, readOnlyHint: false, destructiveHint: false, }); - expect(remove?.annotations).toEqual({ readOnlyHint: false, destructiveHint: true }); + expect(remove?.annotations).toEqual({ + ...defaults, + readOnlyHint: false, + destructiveHint: true, + }); expect(remove?._meta?.risk).toBe("destructive"); expect(list?.inputSchema).toMatchObject({ type: "object", @@ -225,6 +235,7 @@ describe("createMcpServerKit", () => { idempotentHint: true, openWorldHint: false, readOnlyHint: true, + destructiveHint: false, }); const called = await client.callTool({ diff --git a/packages/mcp-server-kit/src/create.ts b/packages/mcp-server-kit/src/create.ts index efd224f..9648565 100644 --- a/packages/mcp-server-kit/src/create.ts +++ b/packages/mcp-server-kit/src/create.ts @@ -36,7 +36,10 @@ export type McpToolDefinition = { * against it. */ outputSchema?: z.ZodType; - /** Hints beside the ones `risk` sets (`readOnlyHint`, `destructiveHint`). */ + /** + * Hints beside the ones `risk` sets (`readOnlyHint`, `destructiveHint`). + * Every hint is advertised as an explicit boolean; these default to false. + */ annotations?: Pick; handler: (input: { context: Context; @@ -92,11 +95,14 @@ function toolAnnotations( risk: McpToolRisk, hints: McpToolDefinition["annotations"], ): ToolAnnotations { - const riskHints = - risk === "read" - ? { readOnlyHint: true } - : { readOnlyHint: false, destructiveHint: risk === "destructive" }; - return { ...hints, ...riskHints }; + // Directories (ChatGPT) want every hint as an explicit boolean. Product + // tools act on the tenant's own workspace unless they say otherwise. + return { + idempotentHint: hints?.idempotentHint ?? false, + openWorldHint: hints?.openWorldHint ?? false, + readOnlyHint: risk === "read", + destructiveHint: risk === "destructive", + }; } /**