Conversation
Claude Code and other MCP clients have timeout issues with long-running tool calls. This adds an opt-in async mode that replaces blocking tools with start_job (returns immediately with job_id) and get_job (polls for status/result). Usage: visor mcp-server --config workflow.yaml --async visor --mcp --mcp-auth-token secret --mcp-async Features: - JobManager with 6-char hex job IDs, idempotency keys, TTL cleanup - Consistent response shape with progress, polling hints, and model instructions - Works for both standalone (mcp-server) and runner (--mcp) modes - Configurable via CLI flags, YAML config, or VISOR_MCP_ASYNC env var Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Overview: Async Job Mode for MCP ServerSummaryThis PR introduces an opt-in async job mode for the MCP (Model Context Protocol) server to address timeout limitations in Claude Code and other MCP clients when handling long-running tool calls. The implementation uses a long polling pattern ( Files ChangedNew Files
Modified Files
Architecture & Impact AssessmentWhat This PR Accomplishes
Key Technical ChangesJobManager Class (
|
| Method | Example |
|---|---|
| CLI (standalone) | visor mcp-server --config workflow.yaml --async --poll-timeout 30 |
| CLI (runner) | visor --mcp --mcp-auth-token secret --mcp-async |
| Environment | VISOR_MCP_ASYNC=true VISOR_MCP_POLL_TIMEOUT=30 visor --mcp |
| Config file | mcp_server: { async_mode: true, long_poll_timeout: 30 } |
Scope Discovery & Context Expansion
Directly Affected Modules
- MCP Server Core (
src/mcp-server.ts) — Tool registration, TaskStore integration - MCP Server Runner (
src/runners/mcp-server-runner.ts) — Runner integration with TaskStore - CLI Layer (
src/cli.ts,src/cli-main.ts) — Flag parsing - Type Definitions (
src/types/cli.ts) — Interface updates
Related Components
- TaskStore (
src/agent-protocol/task-store) — SQLite persistence layer for jobs - Workflow Executor — Called by async job handlers
- Session Management —
session_idparameter preserved in async mode - Agent Protocol — Task state management and lifecycle
- A2A Frontend — Also uses TaskStore for HTTP task management
Testing Coverage
- Unit Tests: 14 tests covering:
- Job lifecycle (start → complete → get)
- Long polling behavior (early return, timeout)
- Failure handling (Error and non-Error thrown values)
- Job ID format (8-char hex) and prefix lookup
- TaskStore visibility and metadata
- Concurrent getJob calls
- Configurable timeout
- Full response shape validation
Potential Review Focus Areas
- TaskStore Lifecycle: JobManager creates TaskStore if not provided — ensure proper initialization/shutdown
- Heartbeat Timer: Uses
unref()to avoid blocking process exit - Error Handling: Failed jobs include
retryable: trueflag; silent catch blocks for state transitions - Backward Compatibility: Async mode is opt-in; existing blocking behavior unchanged
- Result Extraction: Complex logic to extract text from various result formats (MCP content, engine results)
- Security Considerations: No job ownership validation (any authenticated client can access any job_id), 8-char IDs have limited entropy (32 bits)
Usage Example
# Start MCP server in async mode
visor mcp-server --config workflow.yaml --async
# Client flow:
# 1. Call start_job with workflow parameters
# 2. Receive immediate response with job_id (8-char hex)
# 3. Poll get_job (waits up to 59s per call)
# 4. Extract result from final response when done=true
# Jobs are visible via:
visor tasks list # Shows async jobs alongside regular tasks
visor tasks show <id> # Full task detailsReferences
Code Locations
src/mcp-job-manager.ts:1-378— JobManager class implementationsrc/mcp-job-manager.ts:237-343—startJob()method with task creation and background executionsrc/mcp-job-manager.ts:345-393—getJob()method with long polling logicsrc/mcp-job-manager.ts:395-410—resolveTask()for short ID prefix lookupsrc/mcp-server.ts:92-99—McpServerOptionsinterface (asyncMode, longPollTimeout, taskStore)src/mcp-server.ts:521-615—registerAsyncJobTools()functionsrc/mcp-server.ts:515-519—getOrCreateTaskStore()helpersrc/mcp-server.ts:683-688— Async mode conditional increateHttpMcpServer()src/mcp-server.ts:921-928— Async mode conditional instartMcpServer()src/runners/mcp-server-runner.ts:29-35—McpFrontendOptionsinterfacesrc/runners/mcp-server-runner.ts:111-189— Async tool registration in runnersrc/cli.ts:45—--mcp-asyncCLI flag definitionsrc/cli-main.ts:1319-1320—--asyncand--poll-timeoutflag parsingsrc/types/cli.ts:80-81—CliOptions.mcpAsynctype definitionsrc/runners/runner-factory.ts:153-161— Async mode option passingtests/unit/mcp-job-manager.test.ts:1-306— Unit tests
Metadata
- Review Effort: 3 / 5
- Primary Label: feature
Powered by Visor from Probelabs
Last updated: 2026-03-21T17:43:57.734Z | Triggered by: pr_updated | Commit: bb362b2
💡 TIP: You can chat with Visor using /visor ask <your question>
Security Issues (8)
Architecture Issues (11)
Quality Issues (27)
Powered by Visor from Probelabs Last updated: 2026-03-21T17:37:12.081Z | Triggered by: pr_updated | Commit: bb362b2 💡 TIP: You can chat with Visor using |
Instead of a standalone in-memory Map, JobManager now delegates to the existing TaskStore (SQLite-backed). Jobs are stored as regular Visor tasks, visible via `visor tasks list` and `visor tasks show`. - Removed duplicate storage, cleanup timers, and idempotency index - Job IDs are first 8 chars of UUID (matching `visor tasks` CLI format) - start_job creates a task, transitions to working, runs handler async - get_job queries TaskStore by ID (supports short ID prefix lookup) - MCP server creates a SqliteTaskStore if none is provided Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The get_job long poll timeout is now configurable via --poll-timeout CLI flag, long_poll_timeout config option, or VISOR_MCP_POLL_TIMEOUT env var. Defaults to 59 seconds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tests for plain string results, non-Error thrown values, concurrent getJob calls, and full response shape validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Log warning instead of silently ignoring state update failures in error handler - Fix misleading test comment and tighten timing assertions - Use named constant for timeout and validate bounds precisely Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
start_job/get_job) for MCP server to work around Claude Code's long-running tool call timeout limitationsget_jobwhich uses long polling (waits up to 59 seconds by default)visor tasks listandvisor tasks showvisor mcp-server --async) and runner (visor --mcp --mcp-async) modesHow It Works
start_jobwith the message/workflow parametersjob_id(8-char UUID prefix)get_jobwith thejob_id— the server holds the connection open for up to 59 seconds (configurable), checking every 500msget_jobagainThis drastically reduces round-trips compared to short-interval polling. A 10-minute job requires ~10 polls (59s each) instead of ~60 polls (10s each).
Response Shape
Every response from both
start_jobandget_jobuses the same standardized shape:{ "job_id": "a1b2c3d4", "status": "queued|running|completed|failed|expired", "done": false, "progress": { "percent": 0, "step": "queued", "message": "Job accepted" }, "polling": { "recommended_next_action": "get_job", "recommended_delay_seconds": 0 }, "result": null, "error": null, "user_message": "The job has started.", "next_instruction_for_model": "Call get_job with this job_id..." }Changes
src/mcp-job-manager.ts(NEW)JobManagerclass — thin wrapper over TaskStore with long polling, 8-char job IDssrc/mcp-server.tsasyncMode+longPollTimeoutoptions; registersstart_job/get_jobwhen enabledsrc/runners/mcp-server-runner.tssrc/runners/runner-factory.tsasyncModeandlongPollTimeoutfrom CLI/config/envsrc/cli.ts/src/types/cli.ts--mcp-asyncCLI flagsrc/cli-main.ts--asyncand--poll-timeoutflags formcp-serversubcommandtests/unit/mcp-job-manager.test.ts(NEW)Configuration
Test plan
--async, callstart_jobandget_jobvia curlget_jobfor long-running workflows🤖 Generated with Claude Code