From ac3b371ead1983d96b0c9876b3627d985b19fd70 Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Sun, 1 Nov 2026 16:25:49 +0530 Subject: [PATCH] feat(mcp-server-kit): advertise every tool hint as an explicit boolean The ChatGPT Plugins Directory requires readOnlyHint, destructiveHint and openWorldHint to be explicit on every tool. Read tools left destructiveHint out and tools without annotations left out openWorldHint, so a product on the kit could not pass review. idempotentHint and openWorldHint now default to false (tools act on the tenant's own workspace unless they say otherwise). --- .changeset/mcp-explicit-hints.md | 9 +++++++++ packages/mcp-server-kit/src/create.test.ts | 15 +++++++++++++-- packages/mcp-server-kit/src/create.ts | 18 ++++++++++++------ 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 .changeset/mcp-explicit-hints.md 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", + }; } /**