Skip to content

Run background bash commands as MCP tasks - #164

Merged
gartnera merged 5 commits into
mainfrom
claude/sleepy-gauss-3ckcqk
Nov 7, 2026
Merged

gartnera merged 5 commits into
mainfrom
claude/sleepy-gauss-3ckcqk

Conversation

@gartnera

@gartnera gartnera commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Summary

A run_in_background bash call from a client that declares the MCP tasks extension (io.modelcontextprotocol/tasks, protocol revision 2026-07-28) is now answered with a task instead of a shell id. Claude Code (2.1.304+) then shows the command in /tasks, lets the user stop it there, and wakes Claude with the result when it finishes, as it does for its own background Bash.

  • The task result carries the exit code, the output file, and the last 8 KiB of output. A non-zero exit or a kill is reported as an error. tasks/cancel kills the command.
  • Control tools: kill_shell and list_shells are neither listed for nor callable by a client that runs tasks. Every other client keeps the shell-id reply and those tools.
  • Foreground calls are still answered directly.

Breaking: MCP clients that declare the tasks extension no longer see kill_shell / list_shells, and they get a task handle where they used to get a shell id.

Dependency: mcp-go fork

  • mcp-go upgrade: v0.44.0 → v1.5.0, which speaks protocol 2026-07-28.
  • Why a fork: no mcp-go release supports the tasks extension, so go.mod replaces it with gartnera/mcp-go main.
  • What the fork carries: upstream main, plus feat: add SEP-2663 Tasks Extension mark3labs/mcp-go#987 (the extension, still unmerged upstream), plus server.WithTaskDecision. That option lets the server keep a call synchronous; without it, every bash call would become a task once the client declares the extension.
  • Removal: a TODO in go.mod says to drop the fork once upstream supports both.

Known limitations (documented in docs/background-processes.md)

  • Subagents: a Claude Code subagent shares its session's tool list but never gets tasks. In a task-capable session, its background commands get a shell id with no control tools offered.
  • Refused commands: a background command the sandbox refuses fails on the task's first poll, not as an immediate error.
  • When Claude Code uses tasks: only in some sessions (the interactive CLI, and Agent SDK sessions with CLAUDE_AUTO_BACKGROUND_TASKS=1). Never for one-shot claude -p runs or for subagents.

Tests

  • Unit: go test ./... passes. cmd/serve_tasks_test.go covers the control-tool filter, the task result, foreground calls, plain clients, and cancellation. OutputTail has its own test.
  • Mocked e2e (LITE_SANDBOX_E2E=1 go test ./e2e/mockedserver/): passes, with a new TestBackgroundCommands. Every agent tested (Claude Code -p, Codex, Crush, Grok, opencode v1, pi) gets the shell-id path and can call list_shells. opencode v2 is not covered.
  • Version bump: Claude Code is now pinned at 2.1.319. That version turns on tool search behind an ANTHROPIC_BASE_URL gateway, so the launch layering test now also accepts a user's MCP server announced as a deferred tool.
  • Real-model e2e (e2e/claude): passes, with a new test_background_task_e2e.py. Claude Code runs the command as a task, has no control tools, and reports output it can only learn from the task's result. The Agent SDK is bumped to 0.2.179, which bundles CLI 2.1.319.
  • Not run: OS_SANDBOX_TESTS=1, and the docs link check (lychee wasn't available).

Generated by Claude Code

v1.5.0 speaks MCP protocol revision 2026-07-28 (server/discover,
per-request _meta capabilities), which Claude Code's v2 MCP runtime
negotiates and which its MCP tasks support requires. Groundwork for
surfacing background bash commands as MCP tasks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BqmpXXbY9uL5rUYrAjCnKg
A run_in_background bash call from a client that declares the MCP tasks
extension (io.modelcontextprotocol/tasks, protocol 2026-07-28) is now
answered with a task: the client follows it, is told when it finishes
with its exit code, output file, and the end of its output, and stops it
with tasks/cancel. Claude Code shows such commands in /tasks and wakes
the agent when they end. kill_shell and list_shells are hidden from such
a client; every other client keeps the shell id reply and those tools.

mcp-go does not release the extension yet, so go.mod replaces it with
the gartnera/mcp-go fork (upstream main + mark3labs/mcp-go#987 +
server.WithTaskDecision, which keeps foreground calls synchronous).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BqmpXXbY9uL5rUYrAjCnKg
…1.319

TestBackgroundCommands runs a background command through each agent and
follows it with list_shells: none of the non-interactive runs gets MCP
tasks, so each must keep the shell id reply and the control tools.

Claude Code 2.1.319 (which has MCP tasks) turns on tool search behind an
ANTHROPIC_BASE_URL gateway, so a user's MCP server that launch does not
mark always-load is announced as a deferred tool rather than offered
outright; the layering test accepts either. The Agent SDK used by the
real-model suite is bumped to 0.2.179, which bundles the same CLI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BqmpXXbY9uL5rUYrAjCnKg
test_background_task_e2e runs a background command through Claude Code
with MCP tasks on and checks that it runs as a task: the session is not
offered kill_shell or list_shells, the call is not answered with a shell
id, and Claude reports the output once the task completes. conftest now
also clears the variables a surrounding Claude Code cloud session sets,
which would otherwise keep the CLI under test from using MCP tasks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BqmpXXbY9uL5rUYrAjCnKg
…dary

- The real-model task test now requires output Claude can only know from
  the task's result: the command computes it, and only text after the
  completion notification counts. It catches asyncio.TimeoutError, which
  is not the builtin TimeoutError on Python 3.10.
- The mocked background test no longer races list_shells against the
  background echo finishing.
- OutputTail starts a single long line on a character boundary, and
  avoids copying the buffer to find a newline.
- Document that subagents in a task session aren't offered the control
  tools, and that refused commands fail on the task's first poll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BqmpXXbY9uL5rUYrAjCnKg
@gartnera gartnera added the release:major Breaking change: bump the major version (minor while the project is on 0.x) label Nov 7, 2026 — with Claude
@gartnera
gartnera enabled auto-merge (squash) November 7, 2026 00:22
@gartnera
gartnera merged commit e1b0088 into main Nov 7, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:major Breaking change: bump the major version (minor while the project is on 0.x)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants