diff --git a/src/meta/AGENTS.md b/src/meta/AGENTS.md index 1cae89f..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: 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 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 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, + }); + } + }); + }); });