Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-mcp-response-tag.md
Original file line number Diff line number Diff line change
@@ -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).
29 changes: 21 additions & 8 deletions apps/mcp/tests/integration/tools/generate.tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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' }
});
Expand Down Expand Up @@ -66,12 +66,25 @@ describe('generate MCP tool', () => {
* The inspector returns MCP protocol format: { content: [{ type: "text", text: "<json>" }] }
*/
const callMCPTool = (toolName: string, args: Record<string, any>): 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',
Expand Down
27 changes: 20 additions & 7 deletions apps/mcp/tests/integration/tools/get-tasks.tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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' }
});
Expand Down Expand Up @@ -66,12 +66,25 @@ describe('get_tasks MCP tool', () => {
* The inspector returns MCP protocol format: { content: [{ type: "text", text: "<json>" }] }
*/
const callMCPTool = (toolName: string, args: Record<string, any>): 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' }
);

Expand Down
130 changes: 130 additions & 0 deletions apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts
Original file line number Diff line number Diff line change
@@ -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)
);
Comment on lines +76 to +82
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace direct JSON writes with the project JSON utility.

Line 76 and Line 79 use fs.writeFileSync for JSON. This bypasses the shared JSON utility and its validation/error-handling conventions.

As per coding guidelines, "Use readJSON and writeJSON utilities for all JSON file operations instead of raw fs.readFileSync or fs.writeFileSync" and "Include error handling for JSON file operations and validate JSON structure after reading."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts` around lines 76
- 82, Replace the direct fs.writeFileSync calls with the project's JSON utility:
import and use writeJSON to write tasksPath with data and statePath with {
currentTag: 'master' } (instead of fs.writeFileSync). Ensure you add error
handling around the write operations (catch/rethrow or assert) and, after
writing, optionally read back via readJSON to validate the written structure
matches expectations (e.g., tasks shape and state.currentTag) so the test
follows the project's readJSON/writeJSON validation conventions.

});

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<string, string>): 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);
});
3 changes: 2 additions & 1 deletion mcp-server/src/tools/add-subtask.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/add-task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/analyze.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/clear-subtasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/expand-all.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/expand-task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/fix-dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
12 changes: 8 additions & 4 deletions mcp-server/src/tools/move-task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -180,7 +181,8 @@ export function registerMoveTaskTool(server) {
},
log,
errorPrefix: 'Error moving multiple tasks',
projectRoot: args.projectRoot
projectRoot: args.projectRoot,
tag: resolvedTag
});
}
return handleApiResult({
Expand All @@ -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
Expand All @@ -213,7 +216,8 @@ export function registerMoveTaskTool(server) {
),
log,
errorPrefix: 'Error moving task',
projectRoot: args.projectRoot
projectRoot: args.projectRoot,
tag: resolvedTag
});
}
}
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/next-task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/parse-prd.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/remove-dependency.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/remove-subtask.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
3 changes: 2 additions & 1 deletion mcp-server/src/tools/remove-task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
Loading