Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/mcp-explicit-hints.md
Original file line number Diff line number Diff line change
@@ -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`.
15 changes: 13 additions & 2 deletions packages/mcp-server-kit/src/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -225,6 +235,7 @@ describe("createMcpServerKit", () => {
idempotentHint: true,
openWorldHint: false,
readOnlyHint: true,
destructiveHint: false,
});

const called = await client.callTool({
Expand Down
18 changes: 12 additions & 6 deletions packages/mcp-server-kit/src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ export type McpToolDefinition<Context> = {
* 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<ToolAnnotations, "idempotentHint" | "openWorldHint">;
handler: (input: {
context: Context;
Expand Down Expand Up @@ -92,11 +95,14 @@ function toolAnnotations(
risk: McpToolRisk,
hints: McpToolDefinition<unknown>["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",
};
}

/**
Expand Down
Loading