Skip to content

Commit 9b4a563

Browse files
Refined test setup
1 parent f32e234 commit 9b4a563

23 files changed

+469
-335
lines changed

.cursor/rules/errors.mdc

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
description:
3+
globs: **/errors.ts
4+
alwaysApply: false
5+
---
6+
# Error Flow
7+
8+
```mermaid
9+
graph TD
10+
subgraph Core_Logic
11+
FS[FileSystemService: e.g., FileReadError] --> TM[TaskManager: Throws App Errors, e.g., ProjectNotFound, TaskNotDone]
12+
TM -->|Untagged App Error| CLI_Handler["cli.ts Command Handler"]
13+
TM -->|Untagged App Error| ToolExec["toolExecutors.ts: execute"]
14+
end
15+
16+
subgraph CLI_Path
17+
CLI_Handler -->|Untagged App Error| CLI_Catch["cli.ts catch block"]
18+
CLI_Catch -->|Error Object| FormatCLI["client errors.ts formatCliError"]
19+
FormatCLI -->|Formatted String| ConsoleOut["console.error Output"]
20+
end
21+
22+
subgraph MCP_Server_Path
23+
subgraph Validation
24+
ToolExecVal["toolExecutors.ts Validation"] -->|Throws Tagged Protocol Error jsonRpcCode -32602| ExecToolErrHandler
25+
end
26+
27+
subgraph Execution
28+
ToolExec -->|Untagged App Error| ExecToolErrHandler["tools.ts executeToolAndHandleErrors catch block"]
29+
end
30+
31+
ExecToolErrHandler -->|Error Object| CheckTag["Check if error has jsonRpcCode"]
32+
CheckTag -- Tagged Error --> ReThrow["Re-throw Tagged Error"]
33+
CheckTag -- Untagged App Error --> NormalizeErr["utils errors.ts normalizeError"]
34+
NormalizeErr -->|McpError Object code -32000| FormatResult["Format as isError true result"]
35+
FormatResult -->|content list with isError true| SDKHandler["server index.ts SDK Handler"]
36+
37+
ReThrow -->|Tagged Protocol Error| SDKHandler
38+
SDKHandler -- Tagged Error --> SDKFormatError["SDK Formats Top-Level Error"]
39+
SDKHandler -- isError true Result --> SDKFormatResult["SDK Formats Result Field"]
40+
41+
SDKFormatError -->|JSON-RPC Error Response| MCPClient["MCP Client"]
42+
SDKFormatResult -->|JSON-RPC Success Response with error details| MCPClient
43+
end
44+
```
45+
46+
**Explanation of Error Flow and Transformations:**
47+
48+
Errors primarily originate from two places:
49+
50+
1. **Core Logic (`TaskManager`, `FileSystemService`):** These modules throw standard JavaScript `Error` objects, often subclassed (e.g., `ProjectNotFoundError`, `FileReadError`) but *without* any special MCP/JSON-RPC tagging (`jsonRpcCode`). These represent application-specific or file system problems.
51+
2. **Tool Executors (`toolExecutors.ts`) Validation:** Before calling `TaskManager`, the executors validate input arguments. If validation fails, they create a *new* `Error` object and explicitly *tag* it with `jsonRpcCode = -32602` (Invalid Params).
52+
53+
The handling differs significantly between the CLI and the MCP Server:
54+
55+
**1. CLI Error Path (`cli.ts`)**
56+
57+
1. **Origination:** An untagged application error (e.g., `ProjectNotFoundError`) is thrown by `TaskManager`.
58+
2. **Propagation:** The error propagates directly up the call stack to the `catch` block within the specific command's action handler in `cli.ts`.
59+
3. **Transformation (`formatCliError`):**
60+
* The `catch` block calls `formatCliError` from `src/client/errors.ts`.
61+
* `formatCliError` takes the raw `Error` object.
62+
* It checks the error's `name` (e.g., 'ReadOnlyFileSystemError', 'FileReadError') to provide specific user-friendly messages for known file system issues.
63+
* For other errors, it checks if the error has a `.code` property (like the internal `ErrorCode` enum values, e.g., 'ERR_2000') and prepends it to the error message.
64+
* **Shape Change:** `Error` object -> Formatted `string` suitable for console output.
65+
4. **Output:** The formatted string is printed to `console.error`.
66+
67+
**2. MCP Server Error Path (`server/index.ts` via `tools.ts`)**
68+
69+
1. **Origination:**
70+
* **Validation Error:** A *tagged* protocol error (`jsonRpcCode = -32602`) is thrown by `toolExecutors.ts` validation.
71+
* **Execution Error:** An *untagged* application error (e.g., `TaskNotDone`) is thrown by `TaskManager`.
72+
2. **Catching (`executeToolAndHandleErrors`):** Both types of errors are caught by the `try...catch` block in `executeToolAndHandleErrors` within `src/server/tools.ts`.
73+
3. **Branching & Transformation:**
74+
* **If Tagged Protocol Error:** `executeToolAndHandleErrors` detects the `jsonRpcCode` property and *re-throws* the error unchanged.
75+
* **If Untagged App Error:**
76+
* `executeToolAndHandleErrors` calls `normalizeError` from `src/utils/errors.ts`.
77+
* `normalizeError` takes the raw `Error` object.
78+
* It converts the error into an `McpError` object, typically assigning the `code` to `-32000` (Server Error - a generic JSON-RPC code for implementation-defined errors). It preserves the original error message (stripping any internal `[ERR_CODE]` prefix) and potentially includes the stack trace in the `data` field for debugging.
79+
* **Shape Change:** Raw `Error` object -> Standardized `McpError` object (with JSON-RPC code).
80+
* `executeToolAndHandleErrors` then formats this `McpError` into the MCP `isError: true` structure: `{ content: [{ type: "text", text: "Tool execution failed: <normalized_message>" }], isError: true }`.
81+
* **Shape Change:** `McpError` object -> MCP Tool Result `object` with `isError: true`.
82+
4. **SDK Handling (`@modelcontextprotocol/sdk Server`):** The MCP SDK `Server` instance (used in `server/index.ts`) handles the outcome from `executeToolAndHandleErrors`:
83+
* **If Error was Re-thrown (Tagged Protocol Error):** The SDK catches the *thrown* error. It automatically formats this into a standard JSON-RPC top-level error response (e.g., `{"jsonrpc": "2.0", "error": {"code": -32602, "message": "...", "data": ...}, "id": ...}`).
84+
* **If `isError: true` Object was Returned (Normalized App Error):** The SDK receives the *returned* object. It treats this as a *successful* tool execution from a protocol perspective, but one where the tool itself reported an error. It formats a standard JSON-RPC success response, placing the `isError: true` object inside the `result` field (e.g., `{"jsonrpc": "2.0", "result": {"content": [...], "isError": true}, "id": ...}`).
85+
5. **Output:** The final JSON-RPC response (either an error response or a success response containing an `isError` result) is sent to the connected MCP Client.
86+
87+
**Key Functions Changing Error Shapes:**
88+
89+
1. **`toolExecutors.ts` (Validation Logic):** Creates *new* `Error` objects and *tags* them with `jsonRpcCode`. (Raw Error -> Tagged Error)
90+
2. **`normalizeError` (`src/utils/errors.ts`):** Standardizes various error inputs into `McpError` objects, often using the generic `-32000` code for application errors. (Raw Error -> McpError)
91+
3. **`executeToolAndHandleErrors` (`src/server/tools.ts`):** Packages the `McpError` from `normalizeError` into the MCP-specific `{ content: [...], isError: true }` return format. (McpError -> MCP Result Object)
92+
4. **`formatCliError` (`src/client/errors.ts`):** Converts `Error` objects into user-friendly `string` messages for the CLI. (Error -> String)
93+
5. **`@modelcontextprotocol/sdk Server`:** Formats *thrown*, tagged errors into the top-level JSON-RPC `error` object and *returned* `isError: true` results into the JSON-RPC `result` object. (Tagged Error -> JSON-RPC Error / MCP Result Object -> JSON-RPC Result)

package-lock.json

Lines changed: 40 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,23 @@
33
"version": "1.3.4",
44
"description": "Task Queue MCP Server",
55
"author": "Christopher C. Smith ([email protected])",
6-
"main": "dist/index.js",
6+
"main": "dist/src/server/index.js",
77
"type": "module",
88
"bin": {
9-
"taskqueue-mcp": "dist/index.js",
9+
"taskqueue-mcp": "dist/src/server/index.js",
1010
"taskqueue": "dist/src/client/index.js"
1111
},
1212
"files": [
13-
"dist/index.js",
1413
"dist/src/**/*.js",
1514
"dist/src/**/*.d.ts",
1615
"dist/src/**/*.js.map"
1716
],
1817
"scripts": {
1918
"build": "tsc",
20-
"start": "node dist/index.js",
21-
"dev": "tsc && node dist/index.js",
19+
"start": "node dist/src/server/index.js",
20+
"dev": "tsc && node dist/src/server/index.js",
2221
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
23-
"approve-task": "node dist/src/cli.js approve-task",
24-
"list-tasks": "node dist/src/cli.js list"
22+
"cli": "node dist/src/cli.js"
2523
},
2624
"repository": {
2725
"type": "git",
@@ -39,11 +37,11 @@
3937
"access": "public"
4038
},
4139
"dependencies": {
42-
"@ai-sdk/deepseek": "^0.2.2",
43-
"@ai-sdk/google": "^1.2.3",
44-
"@ai-sdk/openai": "^1.3.4",
40+
"@ai-sdk/deepseek": "^0.2.4",
41+
"@ai-sdk/google": "^1.2.5",
42+
"@ai-sdk/openai": "^1.3.6",
4543
"@modelcontextprotocol/sdk": "^1.8.0",
46-
"ai": "^4.2.8",
44+
"ai": "^4.2.10",
4745
"chalk": "^5.4.1",
4846
"cli-table3": "^0.6.5",
4947
"commander": "^13.1.0",

src/server/TaskManager.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -286,16 +286,16 @@ export class TaskManager {
286286
throw new ProjectAlreadyCompletedError();
287287
}
288288

289-
const nextTask = proj.tasks.find((t) => t.status !== "done");
289+
const nextTask = proj.tasks.find((t) => !(t.status === "done" && t.approved));
290290
if (!nextTask) {
291-
// all tasks done?
292-
const allDone = proj.tasks.every((t) => t.status === "done");
293-
if (allDone && !proj.completed) {
291+
// all tasks done and approved?
292+
const allDoneAndApproved = proj.tasks.every((t) => t.status === "done" && t.approved);
293+
if (allDoneAndApproved && !proj.completed) {
294294
return {
295-
message: `All tasks have been completed. Awaiting project completion approval.`
295+
message: `All tasks have been completed and approved. Awaiting project completion approval.`
296296
};
297297
}
298-
throw new TaskNotFoundError("No undone tasks found");
298+
throw new TaskNotFoundError("No incomplete or unapproved tasks found");
299299
}
300300

301301
return {

index.ts renamed to src/server/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
44
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5-
import { TaskManager } from "./src/server/TaskManager.js";
6-
import { ALL_TOOLS, executeToolAndHandleErrors } from "./src/server/tools.js";
5+
import { TaskManager } from "./TaskManager.js";
6+
import { ALL_TOOLS, executeToolAndHandleErrors } from "./tools.js";
77
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
88

99
// Create server with capabilities BEFORE setting up handlers

tests/cli/cli.integration.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,6 @@ describe("CLI Integration Tests", () => {
168168
`TASK_MANAGER_FILE_PATH=${tasksFilePath} tsx ${CLI_PATH} generate-plan --prompt "Create app" --attachment nonexistent.txt`
169169
).catch(error => ({ stdout: error.stdout, stderr: error.stderr }));
170170

171-
// Keep these console logs temporarily if helpful for debugging during development
172-
// console.log("Test stdout:", stdout);
173-
// console.log("Test stderr:", stderr);
174-
175171
// Updated assertion to match the formatCliError output
176172
expect(stderr).toContain("[ERR_4000] Failed to read attachment file: nonexistent.txt");
177173
expect(stderr).toContain("-> Details: Attachment file not found: nonexistent.txt");

0 commit comments

Comments
 (0)