diff --git a/.changeset/fix-mcp-response-tag.md b/.changeset/fix-mcp-response-tag.md new file mode 100644 index 0000000000..6c1cb44ca1 --- /dev/null +++ b/.changeset/fix-mcp-response-tag.md @@ -0,0 +1,5 @@ +--- +"task-master-ai": patch +--- + +Fix MCP tool responses returning the wrong active tag when a `tag` argument is passed explicitly. Tools like `next_task`, `add_task`, `update_task`, `update_subtask`, `expand_task`, `remove_task`, `move_task` and others now report the requested tag in their response payload instead of falling back to `currentTag` from `.taskmaster/state.json`. Resolves #1683 (and related symptom in #1638). diff --git a/apps/mcp/tests/integration/tools/generate.tool.test.ts b/apps/mcp/tests/integration/tools/generate.tool.test.ts index ec21a1c803..51397b4eea 100644 --- a/apps/mcp/tests/integration/tools/generate.tool.test.ts +++ b/apps/mcp/tests/integration/tools/generate.tool.test.ts @@ -7,7 +7,7 @@ * @integration */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -31,7 +31,7 @@ describe('generate MCP tool', () => { ); // Initialize Task Master in test directory - execSync(`node "${cliPath}" init --yes`, { + execFileSync(process.execPath, [cliPath, 'init', '--yes'], { stdio: 'pipe', env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' } }); @@ -66,12 +66,25 @@ describe('generate MCP tool', () => { * The inspector returns MCP protocol format: { content: [{ type: "text", text: "" }] } */ const callMCPTool = (toolName: string, args: Record): any => { - const toolArgs = Object.entries(args) - .map(([key, value]) => `--tool-arg ${key}=${value}`) - .join(' '); - - const output = execSync( - `npx @modelcontextprotocol/inspector --cli node "${mcpServerPath}" --method tools/call --tool-name ${toolName} ${toolArgs}`, + const toolArgs = Object.entries(args).flatMap(([key, value]) => [ + '--tool-arg', + `${key}=${value}` + ]); + + const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx'; + const output = execFileSync( + npxBin, + [ + '@modelcontextprotocol/inspector', + '--cli', + 'node', + mcpServerPath, + '--method', + 'tools/call', + '--tool-name', + toolName, + ...toolArgs + ], { encoding: 'utf-8', stdio: 'pipe', diff --git a/apps/mcp/tests/integration/tools/get-tasks.tool.test.ts b/apps/mcp/tests/integration/tools/get-tasks.tool.test.ts index 25ae16584a..cc5add63bd 100644 --- a/apps/mcp/tests/integration/tools/get-tasks.tool.test.ts +++ b/apps/mcp/tests/integration/tools/get-tasks.tool.test.ts @@ -7,7 +7,7 @@ * @integration */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -31,7 +31,7 @@ describe('get_tasks MCP tool', () => { ); // Initialize Task Master in test directory - execSync(`node "${cliPath}" init --yes`, { + execFileSync(process.execPath, [cliPath, 'init', '--yes'], { stdio: 'pipe', env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' } }); @@ -66,12 +66,25 @@ describe('get_tasks MCP tool', () => { * The inspector returns MCP protocol format: { content: [{ type: "text", text: "" }] } */ const callMCPTool = (toolName: string, args: Record): any => { - const toolArgs = Object.entries(args) - .map(([key, value]) => `--tool-arg ${key}=${value}`) - .join(' '); + const toolArgs = Object.entries(args).flatMap(([key, value]) => [ + '--tool-arg', + `${key}=${value}` + ]); - const output = execSync( - `npx @modelcontextprotocol/inspector --cli node "${mcpServerPath}" --method tools/call --tool-name ${toolName} ${toolArgs}`, + const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx'; + const output = execFileSync( + npxBin, + [ + '@modelcontextprotocol/inspector', + '--cli', + 'node', + mcpServerPath, + '--method', + 'tools/call', + '--tool-name', + toolName, + ...toolArgs + ], { encoding: 'utf-8', stdio: 'pipe' } ); diff --git a/apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts b/apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts new file mode 100644 index 0000000000..1d540ef4ce --- /dev/null +++ b/apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts @@ -0,0 +1,130 @@ +/** + * @fileoverview Regression test for upstream issue #1683 (and #1638) + * + * Verifies that JS-side MCP tools which take a `tag` parameter + * return that same tag back in their response payload, instead of + * silently falling back to the `currentTag` from `.taskmaster/state.json`. + * + * Before the fix, `next_task(tag="phase3")` would respond with + * `{ ..., "tag": "master" }` whenever state.json still pointed at master. + * + * @integration + */ + +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { createTask } from '@tm/core/testing'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +describe('MCP response tag honors explicit tag arg (issue #1683)', () => { + let testDir: string; + let tasksPath: string; + let statePath: string; + let cliPath: string; + let mcpServerPath: string; + + beforeEach(() => { + testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-mcp-1683-')); + process.chdir(testDir); + + cliPath = path.resolve(__dirname, '../../../../../dist/task-master.js'); + mcpServerPath = path.resolve( + __dirname, + '../../../../../dist/mcp-server.js' + ); + + execFileSync(process.execPath, [cliPath, 'init', '--yes'], { + stdio: 'pipe', + env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' } + }); + + tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json'); + statePath = path.join(testDir, '.taskmaster', 'state.json'); + + // Multi-tag tasks file with master + phase3 + const data = { + master: { + tasks: [createTask({ id: 1, title: 'Master task', status: 'pending' })], + metadata: { + version: '1.0.0', + lastModified: new Date().toISOString(), + taskCount: 1, + completedCount: 0, + tags: ['master'] + } + }, + phase3: { + tasks: [ + createTask({ + id: 41, + title: 'Phase3 task', + status: 'pending', + priority: 'high' + }) + ], + metadata: { + version: '1.0.0', + lastModified: new Date().toISOString(), + taskCount: 1, + completedCount: 0, + tags: ['phase3'] + } + } + }; + fs.writeFileSync(tasksPath, JSON.stringify(data, null, 2)); + + // Pin currentTag to master so we can detect a fallback bug + fs.writeFileSync( + statePath, + JSON.stringify({ currentTag: 'master' }, null, 2) + ); + }); + + afterEach(() => { + try { + process.chdir(path.resolve(__dirname, '../../../../..')); + } catch { + process.chdir(os.homedir()); + } + if (testDir && fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true, force: true }); + } + }); + + const callMCPTool = (toolName: string, args: Record): any => { + const toolArgs = Object.entries(args).flatMap(([key, value]) => [ + '--tool-arg', + `${key}=${value}` + ]); + + const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx'; + const output = execFileSync( + npxBin, + [ + '@modelcontextprotocol/inspector', + '--cli', + 'node', + mcpServerPath, + '--method', + 'tools/call', + '--tool-name', + toolName, + ...toolArgs + ], + { encoding: 'utf-8', stdio: 'pipe' } + ); + const mcpResponse = JSON.parse(output); + const resultText = mcpResponse.content[0].text; + return JSON.parse(resultText); + }; + + it('next_task(tag="phase3") response carries tag="phase3" even when state.json points at master', () => { + const data = callMCPTool('next_task', { + projectRoot: testDir, + tag: 'phase3' + }); + expect(data.tag).toBe('phase3'); + }, 30000); +}); diff --git a/mcp-server/src/tools/add-subtask.js b/mcp-server/src/tools/add-subtask.js index c03c5065a4..aa9d4875e3 100644 --- a/mcp-server/src/tools/add-subtask.js +++ b/mcp-server/src/tools/add-subtask.js @@ -116,7 +116,8 @@ export function registerAddSubtaskTool(server) { result, log: log, errorPrefix: 'Error adding subtask', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in addSubtask tool: ${error.message}`); diff --git a/mcp-server/src/tools/add-task.js b/mcp-server/src/tools/add-task.js index 35ec5a8469..49f1139bab 100644 --- a/mcp-server/src/tools/add-task.js +++ b/mcp-server/src/tools/add-task.js @@ -115,7 +115,8 @@ export function registerAddTaskTool(server) { result, log: log, errorPrefix: 'Error adding task', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in add-task tool: ${error.message}`); diff --git a/mcp-server/src/tools/analyze.js b/mcp-server/src/tools/analyze.js index 0e1e1bb3fc..0714005bb5 100644 --- a/mcp-server/src/tools/analyze.js +++ b/mcp-server/src/tools/analyze.js @@ -158,7 +158,8 @@ export function registerAnalyzeProjectComplexityTool(server) { result, log: log, errorPrefix: 'Error analyzing task complexity', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error( diff --git a/mcp-server/src/tools/clear-subtasks.js b/mcp-server/src/tools/clear-subtasks.js index a669503e14..9a098654cc 100644 --- a/mcp-server/src/tools/clear-subtasks.js +++ b/mcp-server/src/tools/clear-subtasks.js @@ -95,7 +95,8 @@ export function registerClearSubtasksTool(server) { result, log: context.log, errorPrefix: 'Error clearing subtasks', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { context.log.error(`Error in clearSubtasks tool: ${error.message}`); diff --git a/mcp-server/src/tools/expand-all.js b/mcp-server/src/tools/expand-all.js index 9c8cf08dfc..abc6d03a32 100644 --- a/mcp-server/src/tools/expand-all.js +++ b/mcp-server/src/tools/expand-all.js @@ -119,7 +119,8 @@ export function registerExpandAllTool(server) { result, log: log, errorPrefix: 'Error expanding all tasks', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error( diff --git a/mcp-server/src/tools/expand-task.js b/mcp-server/src/tools/expand-task.js index 5018d2d8ee..bbaf27a0fb 100644 --- a/mcp-server/src/tools/expand-task.js +++ b/mcp-server/src/tools/expand-task.js @@ -102,7 +102,8 @@ export function registerExpandTaskTool(server) { result, log: log, errorPrefix: 'Error expanding task', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in expand-task tool: ${error.message}`); diff --git a/mcp-server/src/tools/fix-dependencies.js b/mcp-server/src/tools/fix-dependencies.js index a2b982f46e..af3b4f1404 100644 --- a/mcp-server/src/tools/fix-dependencies.js +++ b/mcp-server/src/tools/fix-dependencies.js @@ -76,7 +76,8 @@ export function registerFixDependenciesTool(server) { result, log: context.log, errorPrefix: 'Error fixing dependencies', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { context.log.error(`Error in fixDependencies tool: ${error.message}`); diff --git a/mcp-server/src/tools/move-task.js b/mcp-server/src/tools/move-task.js index 0c887ce68e..9ee7a988bd 100644 --- a/mcp-server/src/tools/move-task.js +++ b/mcp-server/src/tools/move-task.js @@ -103,7 +103,8 @@ export function registerMoveTaskTool(server) { ), log, errorPrefix: 'Error moving tasks between tags', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: args.toTag }); } else { // Within-tag move logic (existing functionality) @@ -180,7 +181,8 @@ export function registerMoveTaskTool(server) { }, log, errorPrefix: 'Error moving multiple tasks', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } return handleApiResult({ @@ -194,7 +196,8 @@ export function registerMoveTaskTool(server) { }, log, errorPrefix: 'Error moving multiple tasks', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } else { // Moving a single task @@ -213,7 +216,8 @@ export function registerMoveTaskTool(server) { ), log, errorPrefix: 'Error moving task', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } } diff --git a/mcp-server/src/tools/next-task.js b/mcp-server/src/tools/next-task.js index 36b65faa71..c365a7a602 100644 --- a/mcp-server/src/tools/next-task.js +++ b/mcp-server/src/tools/next-task.js @@ -90,7 +90,8 @@ export function registerNextTaskTool(server) { result, log: log, errorPrefix: 'Error finding next task', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error finding next task: ${error.message}`); diff --git a/mcp-server/src/tools/parse-prd.js b/mcp-server/src/tools/parse-prd.js index 553e945fd6..77c00c2080 100644 --- a/mcp-server/src/tools/parse-prd.js +++ b/mcp-server/src/tools/parse-prd.js @@ -92,7 +92,8 @@ export function registerParsePRDTool(server) { result, log: log, errorPrefix: 'Error parsing PRD', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in parse_prd: ${error.message}`); diff --git a/mcp-server/src/tools/remove-dependency.js b/mcp-server/src/tools/remove-dependency.js index cb641172c4..43cccc2985 100644 --- a/mcp-server/src/tools/remove-dependency.js +++ b/mcp-server/src/tools/remove-dependency.js @@ -84,7 +84,8 @@ export function registerRemoveDependencyTool(server) { result, log: context.log, errorPrefix: 'Error removing dependency', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { context.log.error(`Error in removeDependency tool: ${error.message}`); diff --git a/mcp-server/src/tools/remove-subtask.js b/mcp-server/src/tools/remove-subtask.js index 7e8b1fc963..30ade5ff84 100644 --- a/mcp-server/src/tools/remove-subtask.js +++ b/mcp-server/src/tools/remove-subtask.js @@ -97,7 +97,8 @@ export function registerRemoveSubtaskTool(server) { result, log: log, errorPrefix: 'Error removing subtask', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in removeSubtask tool: ${error.message}`); diff --git a/mcp-server/src/tools/remove-task.js b/mcp-server/src/tools/remove-task.js index 5621a784e4..6e3c4e96a5 100644 --- a/mcp-server/src/tools/remove-task.js +++ b/mcp-server/src/tools/remove-task.js @@ -92,7 +92,8 @@ export function registerRemoveTaskTool(server) { result, log: log, errorPrefix: 'Error removing task', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in remove-task tool: ${error.message}`); diff --git a/mcp-server/src/tools/research.js b/mcp-server/src/tools/research.js index f743ea8fcf..442a3eeaf3 100644 --- a/mcp-server/src/tools/research.js +++ b/mcp-server/src/tools/research.js @@ -103,7 +103,8 @@ export function registerResearchTool(server) { result, log: log, errorPrefix: 'Error performing research', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in research tool: ${error.message}`); diff --git a/mcp-server/src/tools/scope-down.js b/mcp-server/src/tools/scope-down.js index 9433209b9a..61bbfc5b9d 100644 --- a/mcp-server/src/tools/scope-down.js +++ b/mcp-server/src/tools/scope-down.js @@ -96,7 +96,8 @@ export function registerScopeDownTool(server) { result, log: log, errorPrefix: 'Error scoping down task', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in scope-down tool: ${error.message}`); diff --git a/mcp-server/src/tools/scope-up.js b/mcp-server/src/tools/scope-up.js index 9be10cb29a..7c7fba902c 100644 --- a/mcp-server/src/tools/scope-up.js +++ b/mcp-server/src/tools/scope-up.js @@ -96,7 +96,8 @@ export function registerScopeUpTool(server) { result, log: log, errorPrefix: 'Error scoping up task', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in scope-up tool: ${error.message}`); diff --git a/mcp-server/src/tools/set-task-status.js b/mcp-server/src/tools/set-task-status.js index 0bed7a4044..f8daf83052 100644 --- a/mcp-server/src/tools/set-task-status.js +++ b/mcp-server/src/tools/set-task-status.js @@ -121,7 +121,8 @@ export function registerSetTaskStatusTool(server) { result, log: log, errorPrefix: 'Error setting task status', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(`Error in setTaskStatus tool: ${error.message}`); diff --git a/mcp-server/src/tools/update-subtask.js b/mcp-server/src/tools/update-subtask.js index 512a1a530c..09b07903e0 100644 --- a/mcp-server/src/tools/update-subtask.js +++ b/mcp-server/src/tools/update-subtask.js @@ -119,7 +119,8 @@ export function registerUpdateSubtaskTool(server) { result, log: log, errorPrefix: 'Error updating subtask', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error( diff --git a/mcp-server/src/tools/update-task.js b/mcp-server/src/tools/update-task.js index ccbc8d7b86..1363d2cb5b 100644 --- a/mcp-server/src/tools/update-task.js +++ b/mcp-server/src/tools/update-task.js @@ -126,7 +126,8 @@ export function registerUpdateTaskTool(server) { result, log: log, errorPrefix: 'Error updating task', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error( diff --git a/mcp-server/src/tools/update.js b/mcp-server/src/tools/update.js index 341e4e7cb3..b3b7db38c3 100644 --- a/mcp-server/src/tools/update.js +++ b/mcp-server/src/tools/update.js @@ -96,7 +96,8 @@ export function registerUpdateTool(server) { result, log: log, errorPrefix: 'Error updating tasks', - projectRoot: args.projectRoot + projectRoot: args.projectRoot, + tag: resolvedTag }); } catch (error) { log.error(