diff --git a/.changeset/fix-attempt-completion-error-message.md b/.changeset/fix-attempt-completion-error-message.md new file mode 100644 index 00000000..fa9defc0 --- /dev/null +++ b/.changeset/fix-attempt-completion-error-message.md @@ -0,0 +1,6 @@ +--- +"@perstack/base": patch +"perstack": patch +--- + +fix: return actionable error message when attemptCompletion is blocked by incomplete todos diff --git a/apps/base/src/tools/attempt-completion.test.ts b/apps/base/src/tools/attempt-completion.test.ts index 7eca2ae6..a6d42af7 100644 --- a/apps/base/src/tools/attempt-completion.test.ts +++ b/apps/base/src/tools/attempt-completion.test.ts @@ -7,28 +7,33 @@ describe("attemptCompletion tool", () => { await clearTodo() }) - it("returns empty object when no todos exist", async () => { + it("returns success when no todos exist", async () => { const result = await attemptCompletion() - expect(result).toStrictEqual({}) + expect(result).toStrictEqual({ + content: [{ type: "text", text: JSON.stringify({}) }], + }) }) - it("returns empty object when all todos are completed", async () => { + it("returns success when all todos are completed", async () => { await todo({ newTodos: ["Task 1", "Task 2"] }) await todo({ completedTodos: [0, 1] }) const result = await attemptCompletion() - expect(result).toStrictEqual({}) + expect(result).toStrictEqual({ + content: [{ type: "text", text: JSON.stringify({}) }], + }) }) - it("returns remaining todos when incomplete todos exist", async () => { + it("returns error with instructions when incomplete todos exist", async () => { await todo({ newTodos: ["Task 1", "Task 2", "Task 3"] }) await todo({ completedTodos: [1] }) const result = await attemptCompletion() - expect(result).toStrictEqual({ - remainingTodos: [ - { id: 0, title: "Task 1", completed: false }, - { id: 2, title: "Task 3", completed: false }, - ], - }) + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain( + "Cannot complete: there are incomplete todos remaining.", + ) + expect(result.content[0].text).toContain("[id=0] Task 1") + expect(result.content[0].text).toContain("[id=2] Task 3") + expect(result.content[0].text).toContain("todo tool") }) it("registers tool with correct metadata", () => { @@ -41,7 +46,7 @@ describe("attemptCompletion tool", () => { ) }) - it("returns correct MCP response format", async () => { + it("returns correct MCP response format when no todos", async () => { const mockServer = { registerTool: mock() } registerAttemptCompletion(mockServer as never) const handler = mockServer.registerTool.mock.calls[0][2] diff --git a/apps/base/src/tools/attempt-completion.ts b/apps/base/src/tools/attempt-completion.ts index dc693b41..38791388 100644 --- a/apps/base/src/tools/attempt-completion.ts +++ b/apps/base/src/tools/attempt-completion.ts @@ -1,4 +1,5 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js" import { z } from "zod" import { errorToolResult, successToolResult } from "../lib/tool-result.js" import { getRemainingTodos } from "./todo.js" @@ -7,12 +8,30 @@ export type AttemptCompletionResult = | { remainingTodos: { id: number; title: string; completed: boolean }[] } | Record -export async function attemptCompletion(): Promise { +export async function attemptCompletion(): Promise { const remainingTodos = getRemainingTodos() if (remainingTodos.length > 0) { - return { remainingTodos } + const todoList = remainingTodos.map((t) => ` - [id=${t.id}] ${t.title}`).join("\n") + return { + isError: true, + content: [ + { + type: "text", + text: [ + "Cannot complete: there are incomplete todos remaining.", + "", + "Remaining todos:", + todoList, + "", + "You must either:", + "1. Complete the remaining work, then mark each todo as done using the todo tool (e.g., todo({ completedTodos: [id1, id2] }))", + "2. If the work for a todo is already done, mark it as completed using the todo tool before calling attemptCompletion again", + ].join("\n"), + }, + ], + } } - return {} + return successToolResult({}) } export function registerAttemptCompletion(server: McpServer) { @@ -26,7 +45,7 @@ export function registerAttemptCompletion(server: McpServer) { }, async () => { try { - return successToolResult(await attemptCompletion()) + return await attemptCompletion() } catch (e) { if (e instanceof Error) return errorToolResult(e) throw e