From 61dafeaacb1556fc00c99c619680187c6eea12a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:51:48 +0000 Subject: [PATCH 1/2] fix: mark enable_toolset and disable_toolset as non-destructive Both meta-tools were annotated destructiveHint: true, but neither deletes anything: they change which tools are exposed and can be reversed by the other. Set destructiveHint: false, keep idempotentHint: true, assert the annotations in tests and update the meta intent node. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01JZj3jC148r1VwBYogZpWKL --- src/meta/AGENTS.md | 2 +- src/meta/registerMetaTools.ts | 4 ++-- tests/metaTools.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/meta/AGENTS.md b/src/meta/AGENTS.md index 1cae89f..39e14a0 100644 --- a/src/meta/AGENTS.md +++ b/src/meta/AGENTS.md @@ -20,7 +20,7 @@ Registers meta-tools on MCP servers for runtime toolset management. Called by Se 1. **`_meta` is a reserved toolset key** — If a catalog uses `"_meta"` as a toolset key, ToolRegistry will detect a name collision at registration time. Never use this key in the catalog. 2. **Meta-tools registered via ToolRegistry** — Same collision detection path as user tools. Meta-tools appear in `toolRegistry.list()` and `toolRegistry.listByToolset()`. -3. **`enable_toolset` / `disable_toolset` annotated with `destructiveHint: true`** — Visible to LLM clients via MCP tool annotations. Discovery tools (`list_*`, `describe_*`) use `readOnlyHint: true`. +3. **`enable_toolset` / `disable_toolset` annotated with `destructiveHint: false, idempotentHint: true`** — They change which tools are exposed but destroy nothing, and calling either again is a no-op. Visible to LLM clients via MCP tool annotations. Discovery tools (`list_*`, `describe_*`) use `readOnlyHint: true`. ## Anti-patterns diff --git a/src/meta/registerMetaTools.ts b/src/meta/registerMetaTools.ts index 60db1c1..e303c60 100644 --- a/src/meta/registerMetaTools.ts +++ b/src/meta/registerMetaTools.ts @@ -46,7 +46,7 @@ export function registerMetaTools( { description: "Enable a toolset by name", inputSchema: { name: z.string().describe("Toolset name") }, - annotations: { destructiveHint: true, idempotentHint: true }, + annotations: { destructiveHint: false, idempotentHint: true }, }, async (args: { name: string }) => { const result = await manager.enableToolset(args.name); @@ -62,7 +62,7 @@ export function registerMetaTools( { description: "Disable a toolset by name (state only)", inputSchema: { name: z.string().describe("Toolset name") }, - annotations: { destructiveHint: true, idempotentHint: true }, + annotations: { destructiveHint: false, idempotentHint: true }, }, async (args: { name: string }) => { const result = await manager.disableToolset(args.name); diff --git a/tests/metaTools.test.ts b/tests/metaTools.test.ts index 00e269c..cef709e 100644 --- a/tests/metaTools.test.ts +++ b/tests/metaTools.test.ts @@ -330,4 +330,28 @@ describe("Meta-tools return formats", () => { expect(() => toolRegistry.add("enable_toolset")).toThrow(/collision/i); }); }); + + describe("annotations", () => { + it("marks enable_toolset and disable_toolset as non-destructive and idempotent", () => { + const { tools } = createTestSetup(); + + for (const name of ["enable_toolset", "disable_toolset"]) { + expect(findTool(tools, name)?.annotations).toEqual({ + destructiveHint: false, + idempotentHint: true, + }); + } + }); + + it("marks discovery meta-tools as read-only", () => { + const { tools } = createTestSetup(); + + for (const name of ["list_toolsets", "describe_toolset", "list_tools"]) { + expect(findTool(tools, name)?.annotations).toEqual({ + readOnlyHint: true, + idempotentHint: true, + }); + } + }); + }); }); From f1becfdcf9211f9ce5241798599aeee180f0e322 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:53:00 +0000 Subject: [PATCH 2/2] docs: say repeat toggles have no further effect on state Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01JZj3jC148r1VwBYogZpWKL --- src/meta/AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meta/AGENTS.md b/src/meta/AGENTS.md index 39e14a0..93297e0 100644 --- a/src/meta/AGENTS.md +++ b/src/meta/AGENTS.md @@ -20,7 +20,7 @@ Registers meta-tools on MCP servers for runtime toolset management. Called by Se 1. **`_meta` is a reserved toolset key** — If a catalog uses `"_meta"` as a toolset key, ToolRegistry will detect a name collision at registration time. Never use this key in the catalog. 2. **Meta-tools registered via ToolRegistry** — Same collision detection path as user tools. Meta-tools appear in `toolRegistry.list()` and `toolRegistry.listByToolset()`. -3. **`enable_toolset` / `disable_toolset` annotated with `destructiveHint: false, idempotentHint: true`** — They change which tools are exposed but destroy nothing, and calling either again is a no-op. Visible to LLM clients via MCP tool annotations. Discovery tools (`list_*`, `describe_*`) use `readOnlyHint: true`. +3. **`enable_toolset` / `disable_toolset` annotated with `destructiveHint: false, idempotentHint: true`** — They change which tools are exposed but destroy nothing, and repeating either call has no further effect on state. Visible to LLM clients via MCP tool annotations. Discovery tools (`list_*`, `describe_*`) use `readOnlyHint: true`. ## Anti-patterns