Skip to content

Commit 9e7e8a0

Browse files
authored
Merge pull request #29 from code-rabi/claude/project-thread-gpt0wt
fix: mark enable_toolset and disable_toolset as non-destructive
2 parents f8da3bf + f1becfd commit 9e7e8a0

3 files changed

Lines changed: 27 additions & 3 deletions

File tree

‎src/meta/AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Registers meta-tools on MCP servers for runtime toolset management. Called by Se
2020

2121
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.
2222
2. **Meta-tools registered via ToolRegistry** — Same collision detection path as user tools. Meta-tools appear in `toolRegistry.list()` and `toolRegistry.listByToolset()`.
23-
3. **`enable_toolset` / `disable_toolset` annotated with `destructiveHint: true`** — Visible to LLM clients via MCP tool annotations. Discovery tools (`list_*`, `describe_*`) use `readOnlyHint: true`.
23+
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`.
2424

2525
## Anti-patterns
2626

‎src/meta/registerMetaTools.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function registerMetaTools(
4646
{
4747
description: "Enable a toolset by name",
4848
inputSchema: { name: z.string().describe("Toolset name") },
49-
annotations: { destructiveHint: true, idempotentHint: true },
49+
annotations: { destructiveHint: false, idempotentHint: true },
5050
},
5151
async (args: { name: string }) => {
5252
const result = await manager.enableToolset(args.name);
@@ -62,7 +62,7 @@ export function registerMetaTools(
6262
{
6363
description: "Disable a toolset by name (state only)",
6464
inputSchema: { name: z.string().describe("Toolset name") },
65-
annotations: { destructiveHint: true, idempotentHint: true },
65+
annotations: { destructiveHint: false, idempotentHint: true },
6666
},
6767
async (args: { name: string }) => {
6868
const result = await manager.disableToolset(args.name);

‎tests/metaTools.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,28 @@ describe("Meta-tools return formats", () => {
330330
expect(() => toolRegistry.add("enable_toolset")).toThrow(/collision/i);
331331
});
332332
});
333+
334+
describe("annotations", () => {
335+
it("marks enable_toolset and disable_toolset as non-destructive and idempotent", () => {
336+
const { tools } = createTestSetup();
337+
338+
for (const name of ["enable_toolset", "disable_toolset"]) {
339+
expect(findTool(tools, name)?.annotations).toEqual({
340+
destructiveHint: false,
341+
idempotentHint: true,
342+
});
343+
}
344+
});
345+
346+
it("marks discovery meta-tools as read-only", () => {
347+
const { tools } = createTestSetup();
348+
349+
for (const name of ["list_toolsets", "describe_toolset", "list_tools"]) {
350+
expect(findTool(tools, name)?.annotations).toEqual({
351+
readOnlyHint: true,
352+
idempotentHint: true,
353+
});
354+
}
355+
});
356+
});
333357
});

0 commit comments

Comments
 (0)