feat(mcp): Fix parse-prd tool path resolution

Refactors parse-prd MCP tool to properly handle project root and path resolution, fixing the 'Input file not found: /scripts/prd.txt' error.

Key changes include: Made projectRoot a required parameter, prioritized args.projectRoot over session-derived paths, added validation to prevent parsing in invalid directories (/, home dir), improved error handling with detailed messages, and added creation of output directory if needed.

This resolves issues similar to those fixed in initialize-project, where the tool was incorrectly resolving paths when session context was incomplete.
This commit is contained in:
Eyal Toledano
2025-04-11 02:27:02 -04:00
parent b1b46e38da
commit 5fb302c95b
4 changed files with 79 additions and 13 deletions

View File

@@ -8,7 +8,7 @@
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
"MODEL": "claude-3-7-sonnet-20250219", "MODEL": "claude-3-7-sonnet-20250219",
"PERPLEXITY_MODEL": "sonar-pro", "PERPLEXITY_MODEL": "sonar-pro",
"MAX_TOKENS": 128000, "MAX_TOKENS": 64000,
"TEMPERATURE": 0.2, "TEMPERATURE": 0.2,
"DEFAULT_SUBTASKS": 5, "DEFAULT_SUBTASKS": 5,
"DEFAULT_PRIORITY": "medium" "DEFAULT_PRIORITY": "medium"

View File

@@ -109,7 +109,7 @@ export async function initializeProjectDirect(args, log, context = {}) {
resultData = { resultData = {
message: 'Project initialized successfully.', message: 'Project initialized successfully.',
next_step: next_step:
'Now that the project is initialized, create a PRD file at scripts/prd.txt and use the parse-prd tool to generate initial tasks.', 'Now that the project is initialized, the next step is to create the tasks by parsing a PRD. This will create the tasks folder and the initial task files (tasks folder will be created when parse-prd is run). The parse-prd tool will require a prd.txt file as input (typically found in the project root directory, scripts/ directory). You can create a prd.txt file by asking the user about their idea, and then using the scripts/example_prd.txt file as a template to genrate a prd.txt file in scripts/. You may skip all of this if the user already has a prd.txt file. You can THEN use the parse-prd tool to create the tasks. So: step 1 after initialization is to create a prd.txt file in scripts/prd.txt or confirm the user already has one. Step 2 is to use the parse-prd tool to create the tasks. Do not bother looking for tasks after initialization, just use the parse-prd tool to create the tasks after creating a prd.txt from which to parse the tasks. You do NOT need to reinitialize the project to parse-prd.',
...result // Include details returned by initializeProject ...result // Include details returned by initializeProject
}; };
success = true; success = true;

View File

@@ -5,6 +5,7 @@
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; // Import os module for home directory check
import { parsePRD } from '../../../../scripts/modules/task-manager.js'; import { parsePRD } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js'; import { findTasksJsonPath } from '../utils/path-utils.js';
import { import {
@@ -46,7 +47,7 @@ export async function parsePRDDirect(args, log, context = {}) {
}; };
} }
// Parameter validation and path resolution // --- Parameter validation and path resolution ---
if (!args.input) { if (!args.input) {
const errorMessage = const errorMessage =
'No input file specified. Please provide an input PRD document path.'; 'No input file specified. Please provide an input PRD document path.';
@@ -58,12 +59,51 @@ export async function parsePRDDirect(args, log, context = {}) {
}; };
} }
// Resolve input path (relative to project root if provided) // Validate projectRoot
const projectRoot = args.projectRoot || process.cwd(); if (!args.projectRoot) {
const errorMessage = 'Project root is required but was not provided';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_PROJECT_ROOT', message: errorMessage },
fromCache: false
};
}
const homeDir = os.homedir();
// Disallow invalid projectRoot values
if (args.projectRoot === '/' || args.projectRoot === homeDir) {
const errorMessage = `Invalid project root: ${args.projectRoot}. Cannot use root or home directory.`;
log.error(errorMessage);
return {
success: false,
error: { code: 'INVALID_PROJECT_ROOT', message: errorMessage },
fromCache: false
};
}
// Resolve input path (relative to validated project root)
const projectRoot = args.projectRoot;
log.info(`Using validated project root: ${projectRoot}`);
// Make sure the project root directory exists
if (!fs.existsSync(projectRoot)) {
const errorMessage = `Project root directory does not exist: ${projectRoot}`;
log.error(errorMessage);
return {
success: false,
error: { code: 'PROJECT_ROOT_NOT_FOUND', message: errorMessage },
fromCache: false
};
}
// Resolve input path relative to validated project root
const inputPath = path.isAbsolute(args.input) const inputPath = path.isAbsolute(args.input)
? args.input ? args.input
: path.resolve(projectRoot, args.input); : path.resolve(projectRoot, args.input);
log.info(`Resolved input path: ${inputPath}`);
// Determine output path // Determine output path
let outputPath; let outputPath;
if (args.output) { if (args.output) {
@@ -75,13 +115,19 @@ export async function parsePRDDirect(args, log, context = {}) {
outputPath = path.resolve(projectRoot, 'tasks', 'tasks.json'); outputPath = path.resolve(projectRoot, 'tasks', 'tasks.json');
} }
log.info(`Resolved output path: ${outputPath}`);
// Verify input file exists // Verify input file exists
if (!fs.existsSync(inputPath)) { if (!fs.existsSync(inputPath)) {
const errorMessage = `Input file not found: ${inputPath}`; const errorMessage = `Input file not found: ${inputPath}`;
log.error(errorMessage); log.error(errorMessage);
return { return {
success: false, success: false,
error: { code: 'INPUT_FILE_NOT_FOUND', message: errorMessage }, error: {
code: 'INPUT_FILE_NOT_FOUND',
message: errorMessage,
details: `Checked path: ${inputPath}\nProject root: ${projectRoot}\nInput argument: ${args.input}`
},
fromCache: false fromCache: false
}; };
} }
@@ -118,6 +164,13 @@ export async function parsePRDDirect(args, log, context = {}) {
// Enable silent mode to prevent console logs from interfering with JSON response // Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode(); enableSilentMode();
try { try {
// Make sure the output directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
log.info(`Creating output directory: ${outputDir}`);
fs.mkdirSync(outputDir, { recursive: true });
}
// Execute core parsePRD function with AI client // Execute core parsePRD function with AI client
await parsePRD( await parsePRD(
inputPath, inputPath,

View File

@@ -19,7 +19,7 @@ export function registerParsePRDTool(server) {
server.addTool({ server.addTool({
name: 'parse_prd', name: 'parse_prd',
description: description:
'Parse a Product Requirements Document (PRD) text file to automatically generate initial tasks.', "Parse a Product Requirements Document (PRD) text file to automatically generate initial tasks. Reinitializing the project is not necessary to run this tool. It is recommended to run parse-prd after initializing the project and creating/importing a prd.txt file in the project root's scripts/ directory.",
parameters: z.object({ parameters: z.object({
input: z input: z
.string() .string()
@@ -43,21 +43,34 @@ export function registerParsePRDTool(server) {
.describe('Allow overwriting an existing tasks.json file.'), .describe('Allow overwriting an existing tasks.json file.'),
projectRoot: z projectRoot: z
.string() .string()
.optional()
.describe( .describe(
'Absolute path to the root directory of the project (default: automatically detected from session or CWD)' 'Absolute path to the root directory of the project. Required - ALWAYS SET THIS TO THE PROJECT ROOT DIRECTORY.'
) )
}), }),
execute: async (args, { log, session }) => { execute: async (args, { log, session }) => {
try { try {
log.info(`Parsing PRD with args: ${JSON.stringify(args)}`); log.info(`Parsing PRD with args: ${JSON.stringify(args)}`);
let rootFolder = getProjectRootFromSession(session, log); // Make sure projectRoot is passed directly in args or derive from session
// We prioritize projectRoot from args over session-derived path
let rootFolder = args.projectRoot;
if (!rootFolder && args.projectRoot) { // Only if args.projectRoot is undefined or null, try to get it from session
rootFolder = args.projectRoot; if (!rootFolder) {
log.info(`Using project root from args as fallback: ${rootFolder}`); log.warn(
'projectRoot not provided in args, attempting to derive from session'
);
rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder) {
const errorMessage =
'Could not determine project root directory. Please provide projectRoot parameter.';
log.error(errorMessage);
return createErrorResponse(errorMessage);
} }
}
log.info(`Using project root: ${rootFolder} for PRD parsing`);
const result = await parsePRDDirect( const result = await parsePRDDirect(
{ {