refactor(mcp-server): Prioritize session roots for project path discovery

This commit refactors how the MCP server determines the project root directory, prioritizing the path provided by the client session (e.g., Cursor) for increased reliability and simplification.

Previously, project root discovery relied on a complex chain of fallbacks (environment variables, CWD searching, package path checks) within `findTasksJsonPath`. This could be brittle and less accurate when running within an integrated environment like Cursor.

Key changes:

- **Prioritize Session Roots:** MCP tools (`add-task`, `add-dependency`, etc.) now first attempt to extract the project root URI directly from `session.roots[0].uri`.

- **New Utility `getProjectRootFromSession`:** Added a utility function in `mcp-server/src/tools/utils.js` to encapsulate the logic for extracting and decoding the root URI from the session object.

- **Refactor MCP Tools:** Updated tools (`add-task.js`, `add-dependency.js`) to use `getProjectRootFromSession`.

- **Simplify `findTasksJsonPath`:** Prioritized `args.projectRoot`, removed checks for `TASK_MASTER_PROJECT_ROOT` env var and package directory fallback. Retained CWD search and cache check for CLI compatibility.

- **Fix `reportProgress` Usage:** Corrected parameters in `add-dependency.js`.

This change makes project root determination more robust for the MCP server while preserving discovery mechanisms for the standalone CLI.
This commit is contained in:
Eyal Toledano
2025-04-02 12:33:46 -04:00
parent 3af469b35f
commit e04c16cec6
7 changed files with 1621 additions and 61 deletions

View File

@@ -15,6 +15,270 @@ The MCP server acts as a bridge between external tools (like Cursor) and the cor
- **Flow**: `External Tool (Cursor)` <-> `FastMCP Server` <-> `MCP Tools` (`mcp-server/src/tools/*.js`) <-> `Core Logic Wrappers` (`mcp-server/src/core/direct-functions/*.js`, exported via `task-master-core.js`) <-> `Core Modules` (`scripts/modules/*.js`)
- **Goal**: Provide a performant and reliable way for external tools to interact with Task Master functionality without directly invoking the CLI for every operation.
## Tool Definition and Execution
### Tool Structure
MCP tools must follow a specific structure to properly interact with the FastMCP framework:
```javascript
server.addTool({
name: "tool_name", // Use snake_case for tool names
description: "Description of what the tool does",
parameters: z.object({
// Define parameters using Zod
param1: z.string().describe("Parameter description"),
param2: z.number().optional().describe("Optional parameter description"),
// IMPORTANT: For file operations, always include these optional parameters
file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z.string().optional().describe("Root directory of the project")
}),
// The execute function is the core of the tool implementation
execute: async (args, context) => {
// Implementation goes here
// Return response in the appropriate format
}
});
```
### Execute Function Signature
The `execute` function should follow this exact pattern:
```javascript
execute: async (args, context) => {
// Tool implementation
}
```
- **args**: The first parameter contains all the validated parameters defined in the tool's schema
- You can destructure specific parameters: `const { param1, param2, file, projectRoot } = args;`
- Always pass the full `args` object to direct functions: `await yourDirectFunction(args, context.log);`
- **context**: The second parameter is an object with specific properties provided by FastMCP
- Contains `{ log, reportProgress, session }` - **always destructure these from the context object**
- ✅ **DO**: `execute: async (args, { log, reportProgress, session }) => {}`
- ❌ **DON'T**: `execute: async (args, log) => {}`
### Logging Convention
The `log` object provides standardized logging methods:
```javascript
// Proper logging usage within a tool's execute method
execute: async (args, { log, reportProgress, session }) => {
try {
// Log the start of execution with key parameters (but sanitize sensitive data)
log.info(`Starting ${toolName} with parameters: ${JSON.stringify(args, null, 2)}`);
// For debugging information
log.debug("Detailed operation information", { additionalContext: "value" });
// For warnings that don't prevent execution
log.warn("Warning: potential issue detected", { details: "..." });
// Call the direct function and handle the result
const result = await someDirectFunction(args, log);
// Log successful completion
log.info(`Successfully completed ${toolName}`, {
resultSummary: "brief summary without sensitive data"
});
return handleApiResult(result, log);
} catch (error) {
// Log errors with full context
log.error(`Error in ${toolName}: ${error.message}`, {
errorDetails: error.stack
});
return createErrorResponse(error.message, "ERROR_CODE");
}
}
```
### Progress Reporting Convention
Use `reportProgress` for long-running operations to provide feedback to the client:
```javascript
execute: async (args, { log, reportProgress, session }) => {
// Initialize progress at the start
await reportProgress({ progress: 0, total: 100 });
// For known progress stages, update with specific percentages
for (let i = 0; i < stages.length; i++) {
// Do some work...
// Report intermediate progress
await reportProgress({
progress: Math.floor((i + 1) / stages.length * 100),
total: 100
});
}
// For indeterminate progress (no known total)
await reportProgress({ progress: 1 }); // Just increment without total
// When complete
await reportProgress({ progress: 100, total: 100 });
// Return the result
return result;
}
```
FYI reportProgress object is not arbitrary. It must be { progress: number, total: number }.
### Session Usage Convention
The `session` object contains authenticated session data and can be used for:
1. **Authentication information**: Access user-specific data that was set during authentication
2. **Environment variables**: Access environment variables without direct process.env references (if implemented)
3. **User context**: Check user permissions or preferences
```javascript
execute: async (args, { log, reportProgress, session }) => {
// Check if session exists (user is authenticated)
if (!session) {
log.warn("No session data available, operating in anonymous mode");
} else {
// Access authenticated user information
log.info(`Operation requested by user: ${session.userId}`);
// Access environment variables or configuration via session
const apiKey = session.env?.API_KEY;
// Check user-specific permissions
if (session.permissions?.canUpdateTasks) {
// Perform privileged operation
}
}
// Continue with implementation...
}
```
### Accessing Project Roots through Session
The `session` object in FastMCP provides access to filesystem roots via the `session.roots` array, which can be used to get the client's project root directory. This can help bypass some of the path resolution logic in `path-utils.js` when the client explicitly provides its project root.
#### What are Session Roots?
- The `roots` array contains filesystem root objects provided by the FastMCP client (e.g., Cursor).
- Each root object typically represents a mounted filesystem or workspace that the client (IDE) has access to.
- For tools like Cursor, the first root is usually the current project or workspace root.
#### Roots Structure
Based on the FastMCP core implementation, the roots structure looks like:
```javascript
// Root type from FastMCP
type Root = {
uri: string; // URI of the root, e.g., "file:///Users/username/project"
name: string; // Display name of the root
capabilities?: { // Optional capabilities this root supports
// Root-specific capabilities
}
};
```
#### Accessing Project Root from Session
To properly access and use the project root from the session:
```javascript
execute: async (args, { log, reportProgress, session }) => {
try {
// Try to get the project root from session
let rootFolder = null;
if (session && session.roots && session.roots.length > 0) {
// The first root is typically the main project workspace in clients like Cursor
const firstRoot = session.roots[0];
if (firstRoot && firstRoot.uri) {
// Convert the URI to a file path (strip the file:// prefix if present)
const rootUri = firstRoot.uri;
rootFolder = rootUri.startsWith('file://')
? decodeURIComponent(rootUri.slice(7)) // Remove 'file://' and decode URI components
: rootUri;
log.info(`Using project root from session: ${rootFolder}`);
}
}
// Use rootFolder if available, otherwise let path-utils.js handle the detection
const result = await yourDirectFunction({
projectRoot: rootFolder,
...args
}, log);
return handleApiResult(result, log);
} catch (error) {
log.error(`Error in tool: ${error.message}`);
return createErrorResponse(error.message);
}
}
```
#### Integration with path-utils.js
The `rootFolder` extracted from the session should be passed to the direct function as the `projectRoot` parameter. This integrates with `findTasksJsonPath` in `path-utils.js`, which uses a precedence order for finding the project root:
1. **TASK_MASTER_PROJECT_ROOT** environment variable
2. **Explicitly provided `projectRoot`** (from session.roots or args)
3. Previously found/cached project root
4. Search from current directory upwards
5. Package directory fallback
By providing the rootFolder from session.roots as the projectRoot parameter, we're using the second option in this hierarchy, allowing the client to explicitly tell us where the project is located rather than having to search for it.
#### Example Implementation
Here's a complete example for a tool that properly uses session roots:
```javascript
execute: async (args, { log, reportProgress, session }) => {
try {
log.info(`Starting tool with args: ${JSON.stringify(args)}`);
// Extract project root from session if available
let rootFolder = null;
if (session && session.roots && session.roots.length > 0) {
const firstRoot = session.roots[0];
if (firstRoot && firstRoot.uri) {
rootFolder = firstRoot.uri.startsWith('file://')
? decodeURIComponent(firstRoot.uri.slice(7))
: firstRoot.uri;
log.info(`Using project root from session: ${rootFolder}`);
}
}
// If we couldn't get a root from session but args has projectRoot, use that
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args: ${rootFolder}`);
}
// Call the direct function with the rootFolder
const result = await someDirectFunction({
projectRoot: rootFolder,
...args
}, log);
log.info(`Completed tool successfully`);
return handleApiResult(result, log);
} catch (error) {
log.error(`Error in tool: ${error.message}`);
return createErrorResponse(error.message);
}
}
```
## Key Principles
- **Prefer Direct Function Calls**: For optimal performance and error handling, MCP tools should utilize direct function wrappers (exported via [`task-master-core.js`](mdc:mcp-server/src/core/task-master-core.js), implemented in [`mcp-server/src/core/direct-functions/`](mdc:mcp-server/src/core/direct-functions/)). These wrappers call the underlying logic from the core modules (e.g., [`task-manager.js`](mdc:scripts/modules/task-manager.js)).
@@ -208,7 +472,7 @@ Follow these steps to add MCP support for an existing Task Master command (see [
1. **Ensure Core Logic Exists**: Verify the core functionality is implemented and exported from the relevant module in `scripts/modules/`.
2. **Create Direct Function File in `mcp-server/src/core/direct-functions/`**:
2. **Create Direct Function File in `mcp-server/src/core/direct-functions/`:
- Create a new file (e.g., `your-command.js`) in the `direct-functions` directory using **kebab-case** for file naming.
- Import necessary core functions from Task Master modules.
- Import utilities: **`findTasksJsonPath` from `../utils/path-utils.js`** and `getCachedOrExecute` from `../../tools/utils.js` if needed.
@@ -228,7 +492,7 @@ Follow these steps to add MCP support for an existing Task Master command (see [
- Implement `registerYourCommandTool(server)`.
- Define the tool `name` using **snake_case** (e.g., `your_command`).
- Define the `parameters` using `zod`. **Crucially, if the tool needs project context, include `projectRoot: z.string().optional().describe(...)` and potentially `file: z.string().optional().describe(...)`**. Make `projectRoot` optional.
- Implement the standard `async execute(args, log)` method: call `yourCommandDirect(args, log)` and pass the result to `handleApiResult(result, log, 'Error Message')`.
- Implement the standard `async execute(args, { log, reportProgress, session })` method: call `yourCommandDirect(args, log)` and pass the result to `handleApiResult(result, log, 'Error Message')`.
5. **Register Tool**: Import and call `registerYourCommandTool` in `mcp-server/src/tools/index.js`.
@@ -318,9 +582,9 @@ Follow these steps to add MCP support for an existing Task Master command (see [
- ❌ DON'T: Log entire large data structures or sensitive information
- The MCP server integrates with Task Master core functions through three layers:
1. Tool Definitions (`mcp-server/src/tools/*.js`) - Define parameters and execute methods
2. Direct Function Wrappers (`task-master-core.js`) - Handle validation, path resolution, and caching
3. Core Logic Functions (various modules) - Implement actual functionality
1. Tool Definitions (`mcp-server/src/tools/*.js`) - Define parameters and validation
2. Direct Functions (`mcp-server/src/core/direct-functions/*.js`) - Handle core logic integration
3. Core Functions (`scripts/modules/*.js`) - Implement the actual functionality
- This layered approach provides:
- Clear separation of concerns