Skip to content

fix(mcp): pass resolved tag through to handleApiResult in JS tools#1703

Open
gw1108 wants to merge 3 commits into
eyaltoledano:mainfrom
gw1108:fix/issue-1683-mcp-tag-response
Open

fix(mcp): pass resolved tag through to handleApiResult in JS tools#1703
gw1108 wants to merge 3 commits into
eyaltoledano:mainfrom
gw1108:fix/issue-1683-mcp-tag-response

Conversation

@gw1108
Copy link
Copy Markdown

@gw1108 gw1108 commented May 16, 2026

Summary

Fixes #1683. When an MCP tool is called with an explicit tag argument (e.g. next_task tag=phase3), the response payload was still carrying the currentTag from .taskmaster/state.json (usually master). The data was loaded from the correct tag, but the response's tag field misled callers into thinking the operation ran on a different tag — leading to destructive corrective actions in multi-tag / multi-agent workspaces.

The fix threads the already-resolved tag into handleApiResult in every JS-side MCP tool that wasn't already doing so, matching the existing pattern in add-dependency.js and the TS tools in @tm/mcp.

Changes

  • Updated 19 JS MCP tools under mcp-server/src/tools/ to pass resolvedTag (or args.tag) through to handleApiResult:
    add-subtask, add-task, analyze, clear-subtasks, expand-all, expand-task, fix-dependencies, move-task, next-task, parse-prd, remove-dependency, remove-subtask, remove-task, research, scope-down, scope-up, set-task-status, update-subtask, update-task, update.
  • Added regression test apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts that pins state.json to master, calls next_task with tag=phase3, and asserts the response carries "tag": "phase3".
  • Added changeset.

Test plan

  • npm run test -w apps/mcp passes (includes the new regression test)
  • Manual: in a multi-tag workspace, call next_task / set_task_status / update_task with an explicit tag arg and confirm the response's tag field matches the argument
  • Verify add-dependency.js and TS tools remain unchanged (they already handled this correctly)

Resolves #1683.

Summary by CodeRabbit

  • Bug Fixes

    • MCP tools now report the explicitly requested tag in responses (no longer falling back to a default), ensuring correct tag context across task operations.
  • Tests

    • Added an integration test to verify explicit tag preservation in tool calls.
    • Improved integration test CLI invocation for more reliable, platform-aware execution.

Review Change Stack

When an MCP tool was called with an explicit `tag` argument
(e.g. next_task tag=phase3), the response payload still carried
the `currentTag` from `.taskmaster/state.json` (usually `master`).
The data was loaded from the right tag, but the response's
`tag` field misled callers into thinking the operation ran on a
different tag, which led to destructive corrective actions in
multi-tag/multi-agent workspaces.

The fix threads the already-resolved `tag` into handleApiResult
in every JS-side MCP tool that wasn't already doing so, matching
the existing pattern in add-dependency.js and the TS tools in
@tm/mcp.

Adds a regression test that pins state.json to master, calls
next_task with tag=phase3, and asserts the response carries
"tag": "phase3".

Resolves upstream issue eyaltoledano#1683.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 16, 2026

🦋 Changeset detected

Latest commit: 8a72ef4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
task-master-ai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 520877d1-a86d-4b54-bb55-4c85f84b8a45

📥 Commits

Reviewing files that changed from the base of the PR and between cdd7334 and 8a72ef4.

📒 Files selected for processing (3)
  • apps/mcp/tests/integration/tools/generate.tool.test.ts
  • apps/mcp/tests/integration/tools/get-tasks.tool.test.ts
  • apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts

📝 Walkthrough

Walkthrough

MCP tools now preserve an explicitly provided tag in responses by threading resolved tag into direct calls and into handleApiResult across tools; a changeset and an integration test verify the fix.

Changes

MCP Tool Tag Response Fix

Layer / File(s) Summary
Test and changeset
.changeset/fix-mcp-response-tag.md, apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts
Adds a changeset describing the fix and an integration test that sets state currentTag to a different tag and asserts next_task called with tag: "phase3" returns data.tag === "phase3".
Test infra: execFileSync argv-style calls
apps/mcp/tests/integration/tools/generate.tool.test.ts, apps/mcp/tests/integration/tools/get-tasks.tool.test.ts
Refactors tests to use execFileSync with argv arrays and platform-aware npx selection when invoking the inspector/CLI; updates helper argument construction.
Input-side tag threading to direct functions
mcp-server/src/tools/add-task.js, mcp-server/src/tools/add-subtask.js
add_task and add_subtask now pass tag: resolvedTag into addTaskDirect / addSubtaskDirect calls alongside projectRoot.
Output-side tag threading to result handlers
mcp-server/src/tools/analyze.js, mcp-server/src/tools/clear-subtasks.js, mcp-server/src/tools/expand-all.js, mcp-server/src/tools/expand-task.js, mcp-server/src/tools/fix-dependencies.js, mcp-server/src/tools/move-task.js, mcp-server/src/tools/next-task.js, mcp-server/src/tools/parse-prd.js, mcp-server/src/tools/remove-dependency.js, mcp-server/src/tools/remove-subtask.js, mcp-server/src/tools/remove-task.js, mcp-server/src/tools/research.js, mcp-server/src/tools/scope-down.js, mcp-server/src/tools/scope-up.js, mcp-server/src/tools/set-task-status.js, mcp-server/src/tools/update-subtask.js, mcp-server/src/tools/update-task.js, mcp-server/src/tools/update.js
These tools now include tag: resolvedTag (and projectRoot) in their handleApiResult(...) calls so API responses include the requested tag; move-task uses args.toTag for cross-tag moves and resolvedTag for within-tag paths.

Sequence Diagram(s)

sequenceDiagram
  participant Inspector as MCP Inspector/Client
  participant Tool as MCP Tool Handler
  participant Direct as *Direct Function
  participant Result as handleApiResult
  Inspector->>Tool: call tools/call with tag argument
  Tool->>Tool: resolveTag from args/projectRoot
  Tool->>Direct: call *Direct(..., tag: resolvedTag)
  Direct->>Tool: operation result
  Tool->>Result: handleApiResult(result, { projectRoot, tag: resolvedTag })
  Result->>Inspector: return response payload with tag field
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • Crunchyman-ralph
  • eyaltoledano
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: passing the resolved tag through to handleApiResult in JS tools to fix the tag response issue, directly addressing the core objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mcp-server/src/tools/move-task.js (1)

134-221: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix multi-move control flow and undefined variables in move_task.

Line 134 uses the mismatch condition to enter multi-move handling, which breaks valid multi-move calls and can reference undefined variables (results, skippedMoves) in the fallback branch. Equal-length multi-ID input also incorrectly falls into the single-move path.

Proposed fix
-					// Validate matching IDs count
-					if (fromIds.length !== toIds.length) {
-						if (fromIds.length > 1) {
+					const isMultiMove = fromIds.length > 1 || toIds.length > 1;
+					if (isMultiMove) {
+						if (fromIds.length !== toIds.length) {
+							return createErrorResponse(
+								'For multi-move operations, source and destination ID counts must match',
+								'MISMATCHED_MOVE_COUNTS'
+							);
+						}
 							const results = [];
 							const skipped = [];
 							// Move tasks one by one, only generate files on the last move
 							for (let i = 0; i < fromIds.length; i++) {
@@
 							return handleApiResult({
 								result: {
 									success: true,
 									data: {
 										moves: results,
 										skipped: skipped.length > 0 ? skipped : undefined,
 										message: `Successfully moved ${results.length} tasks${skipped.length > 0 ? `, skipped ${skipped.length}` : ''}`
 									}
 								},
 								log,
 								errorPrefix: 'Error moving multiple tasks',
 								projectRoot: args.projectRoot,
 								tag: resolvedTag
 							});
-						}
-						return handleApiResult({
-							result: {
-								success: true,
-								data: {
-									moves: results,
-									skippedMoves: skippedMoves,
-									message: `Successfully moved ${results.length} tasks${skippedMoves.length > 0 ? `, skipped ${skippedMoves.length} moves` : ''}`
-								}
-							},
-							log,
-							errorPrefix: 'Error moving multiple tasks',
-							projectRoot: args.projectRoot,
-							tag: resolvedTag
-						});
-					} else {
-						// Moving a single task
-						return handleApiResult({
-							result: await moveTaskDirect(
-								{
-									sourceId: args.from,
-									destinationId: args.to,
+					}
+
+					// Moving a single task
+					return handleApiResult({
+						result: await moveTaskDirect(
+							{
+									sourceId: fromIds[0],
+									destinationId: toIds[0],
 									tasksJsonPath,
 									projectRoot: args.projectRoot,
 									tag: resolvedTag,
 									generateFiles: true
-								},
-								log,
-								{ session }
-							),
-							log,
-							errorPrefix: 'Error moving task',
-							projectRoot: args.projectRoot,
-							tag: resolvedTag
-						});
-					}
+							},
+							log,
+							{ session }
+						),
+						log,
+						errorPrefix: 'Error moving task',
+						projectRoot: args.projectRoot,
+						tag: resolvedTag
+					});
🤖 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 `@mcp-server/src/tools/move-task.js` around lines 134 - 221, The multi-move
branch is entered when fromIds.length !== toIds.length which is backwards and
also references undefined variables (results, skippedMoves); change the control
flow so that when fromIds.length === toIds.length && fromIds.length > 1 you
iterate and call moveTaskDirect for each pair (use local arrays results and
skipped), return handleApiResult with data.moves and data.skipped (no
skippedMoves), add a separate branch that returns a clear error via
handleApiResult when fromIds.length !== toIds.length (mismatched counts), and
fall back to the single-move path that calls moveTaskDirect with
args.from/args.to when there is exactly one id; update any variable names to
consistently use results and skipped and reference moveTaskDirect and
handleApiResult accordingly.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts`:
- Around line 38-41: The tests currently call execSync with a shell command
string using cliPath which is injection-prone; replace those calls with
execFileSync using the binary path and an argument array (e.g.,
execFileSync(cliPath, ['init', '--yes'], { stdio: 'pipe', env: { ...process.env,
TASKMASTER_SKIP_AUTO_UPDATE: '1' } })) and do the same for the other occurrence
(the command at lines ~106-109) so both invocations use execFileSync(cliPath,
argsArray, options) instead of execSync(shellString, options).

---

Outside diff comments:
In `@mcp-server/src/tools/move-task.js`:
- Around line 134-221: The multi-move branch is entered when fromIds.length !==
toIds.length which is backwards and also references undefined variables
(results, skippedMoves); change the control flow so that when fromIds.length ===
toIds.length && fromIds.length > 1 you iterate and call moveTaskDirect for each
pair (use local arrays results and skipped), return handleApiResult with
data.moves and data.skipped (no skippedMoves), add a separate branch that
returns a clear error via handleApiResult when fromIds.length !== toIds.length
(mismatched counts), and fall back to the single-move path that calls
moveTaskDirect with args.from/args.to when there is exactly one id; update any
variable names to consistently use results and skipped and reference
moveTaskDirect and handleApiResult accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ac0012d9-a67b-4cd4-8058-724b0060cacb

📥 Commits

Reviewing files that changed from the base of the PR and between c0c98d3 and 7ead0c8.

📒 Files selected for processing (22)
  • .changeset/fix-mcp-response-tag.md
  • apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts
  • mcp-server/src/tools/add-subtask.js
  • mcp-server/src/tools/add-task.js
  • mcp-server/src/tools/analyze.js
  • mcp-server/src/tools/clear-subtasks.js
  • mcp-server/src/tools/expand-all.js
  • mcp-server/src/tools/expand-task.js
  • mcp-server/src/tools/fix-dependencies.js
  • mcp-server/src/tools/move-task.js
  • mcp-server/src/tools/next-task.js
  • mcp-server/src/tools/parse-prd.js
  • mcp-server/src/tools/remove-dependency.js
  • mcp-server/src/tools/remove-subtask.js
  • mcp-server/src/tools/remove-task.js
  • mcp-server/src/tools/research.js
  • mcp-server/src/tools/scope-down.js
  • mcp-server/src/tools/scope-up.js
  • mcp-server/src/tools/set-task-status.js
  • mcp-server/src/tools/update-subtask.js
  • mcp-server/src/tools/update-task.js
  • mcp-server/src/tools/update.js

Comment thread apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts (1)

14-14: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use execFileSync with argument arrays instead of shell-interpolated execSync.

Line 38 and Line 102 build shell command strings with interpolation. Even in tests, this is fragile and can become injection-prone when inputs change. Prefer argument arrays.

Suggested patch
-import { execSync } from 'node:child_process';
+import { execFileSync } from 'node:child_process';
@@
-		execSync(`node "${cliPath}" init --yes`, {
+		execFileSync('node', [cliPath, 'init', '--yes'], {
 			stdio: 'pipe',
 			env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
 		});
@@
-		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}`,
-			{ encoding: 'utf-8', stdio: 'pipe' }
-		);
+		const output = execFileSync(
+			'npx',
+			[
+				'`@modelcontextprotocol/inspector`',
+				'--cli',
+				'node',
+				mcpServerPath,
+				'--method',
+				'tools/call',
+				'--tool-name',
+				toolName,
+				...toolArgs
+			],
+			{ encoding: 'utf-8', stdio: 'pipe' }
+		);
#!/bin/bash
# Verify remaining risky command construction in the updated file.
rg -nP -C2 'execSync\(|execFileSync\(' apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts

Also applies to: 38-41, 96-104

🤖 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` at line 14, The
tests currently use node:child_process.execSync with shell-interpolated command
strings (see usages of execSync in the file) which is fragile and
injection-prone; replace those with execFileSync by importing execFileSync from
'node:child_process' and call execFileSync with the executable as the first
argument and an array of arguments instead of a concatenated shell string
(preserve any options like { encoding: 'utf8', stdio: 'pipe' } or { stdio:
'inherit' } as used), updating the two places that build commands (the blocks
around the current execSync calls) to pass each dynamic value as a separate
array element so no shell interpolation is needed.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts`:
- Around line 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.

---

Duplicate comments:
In `@apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts`:
- Line 14: The tests currently use node:child_process.execSync with
shell-interpolated command strings (see usages of execSync in the file) which is
fragile and injection-prone; replace those with execFileSync by importing
execFileSync from 'node:child_process' and call execFileSync with the executable
as the first argument and an array of arguments instead of a concatenated shell
string (preserve any options like { encoding: 'utf8', stdio: 'pipe' } or {
stdio: 'inherit' } as used), updating the two places that build commands (the
blocks around the current execSync calls) to pass each dynamic value as a
separate array element so no shell interpolation is needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 17054793-bc7d-423d-ae3f-8ba6f97d5cce

📥 Commits

Reviewing files that changed from the base of the PR and between 7ead0c8 and cdd7334.

📒 Files selected for processing (1)
  • apps/mcp/tests/integration/tools/next-task-tag.tool.test.ts

Comment on lines +76 to +82
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)
);
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.

Replaces execSync with shell-string interpolation in three MCP
integration tests (next-task-tag, get-tasks, generate) with
execFileSync using argument arrays. The previous form interpolated
cliPath, mcpServerPath, toolName, and testDir into a shell command,
which is injection-prone and breaks when paths contain spaces.

- Init call: execFileSync(process.execPath, [cliPath, 'init', '--yes'], ...)
  to invoke the .js CLI via the current Node binary.
- MCP inspector call: execFileSync(npxBin, [...argv]) where npxBin is
  'npx.cmd' on Windows and 'npx' elsewhere.
- toolArgs flattened from a space-joined string to a flat argv array.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: set_task_status can drift from tasks.json and next_task can return master for tagged runs

1 participant