Skip to content

Claude Code discuss-milestone preflight can falsely fail workflow MCP tool readiness #808

Description

@rager306

Problem

Claude Code can abort discuss-milestone before the first model turn with:

workflow tool surface not ready for discuss-milestone: MCP server "gsd-workflow" did not register required tools before session start: ask_user_questions, gsd_summary_save, gsd_requirement_save, gsd_requirement_update, gsd_plan_milestone, gsd_milestone_generate_id

The MCP server implementation does register these tools, so the failure is a readiness/preflight false negative or an unreported startup/config failure rather than a missing tool contract.

Root Cause

The Claude Code stream adapter validates workflow MCP readiness before session start by deriving a server name from allowedTools and calling awaitWorkflowMcpToolRegistration:

// extensions/claude-code-cli/stream-adapter.js:1807-1812
const preflightError = await awaitWorkflowMcpToolRegistration({
  unitType: gsdPhase,
  workflowServerName: workflowMcpServerName,
  projectRoot,
  signal: options?.signal,
});

awaitWorkflowMcpToolRegistration then probes by server name only, via persisted MCP config lookup, and discards the probe error:

// extensions/gsd/tool-surface-readiness.js:35-41
const probe = input.probe ?? (async (serverName, root) => {
  const result = await testMcpServerConnection(serverName, {
    projectDir: resolveWorkflowMcpProjectRoot(root),
    timeoutMs: WORKFLOW_MCP_PROBE_TIMEOUT_MS,
  });
  return { ok: result.ok, tools: result.tools };
});

That name-based probe depends on testMcpServerConnection finding a persisted MCP config entry:

// extensions/mcp-client/manager.js:207-220
const config = typeof nameOrConfig === "string"
  ? getMcpServerConfig(nameOrConfig, { projectDir: options.projectDir, includeDisabled: true })
  : nameOrConfig;
if (!config) {
  return { ok: false, tools: [], error: "Unknown MCP server." };
}

But the same Claude Code path builds/injects exact phase MCP server configs separately for the SDK:

// extensions/claude-code-cli/stream-adapter.js:1531-1538
const projectRoot = resolveWorkflowMcpProjectRoot(sdkCwd);
const defaultMcpServers = (() => {
  try {
    return buildProjectGsdMcpServers(projectRoot);
  } catch {
    const workflowServers = buildWorkflowMcpServers(projectRoot) ?? {};
// extensions/claude-code-cli/stream-adapter.js:1616-1621
const inlinePhaseMcpServers = gsdPhase
  ? resolveProjectMcpServerConfigs(projectRoot, [
      workflowExplicitlyBlocked ? undefined : workflowServerName,
      phaseUsesBrowserMcp && !browserExplicitlyBlocked ? browserServerName : undefined,
    ], defaultMcpServers.servers)
  : undefined;

So the preflight and the SDK session can validate different MCP surfaces: the SDK can be given an inline gsd-workflow config while preflight only re-reads persisted config by name. If auto-init failed, was not yet visible, or the config source differs, preflight loops until timeout and reports “did not register required tools,” while hiding the actual probe error.

The server does register the reported tools:

// packages/mcp-server/dist/server.js:841
server.tool('ask_user_questions', ...)

// packages/mcp-server/dist/server.js:1033
registerWorkflowTools(server, ...)

// packages/mcp-server/dist/workflow-tools.js:1444,1465,1486,1499,1639
server.tool("gsd_requirement_update", ...)
server.tool("gsd_requirement_save", ...)
server.tool("gsd_milestone_generate_id", ...)
server.tool("gsd_plan_milestone", ...)
server.tool("gsd_summary_save", ...)

Expected Behavior / Fix Suggestion

Make the preflight validate the same MCP server config that will be passed to the Claude SDK, and preserve probe errors in the timeout message.

Concrete fix options:

  1. Extend awaitWorkflowMcpToolRegistration to accept a normalized workflowServerConfig or mcpServers map.
  2. In stream-adapter.js, pass sdkMcpServers?.[workflowMcpServerName] ?? defaultMcpServers.servers[workflowMcpServerName] into preflight.
  3. In tool-surface-readiness.js, call testMcpServerConnection(configObject, ...) when a config is supplied; only fall back to name lookup when no config is available.
  4. Track lastError from the probe and append it to the timeout message, e.g. last probe error: Unknown MCP server or bridge startup stderr.
  5. Consider making autoInitClaudeCodeWorkflowMcp() return/throw an actionable error instead of silently swallowing failures unless GSD_DEBUG=1.

Suggested regression tests:

  • discuss-milestone phase with inline mcpServers and no persisted .mcp.json should pass preflight when tools/list returns required tools.
  • Auto-init failure should surface the underlying config/write error, not a generic missing-tools timeout.
  • Name-based probe should include the last probe error in did not register required tools before session start messages.

Environment

  • GSD version: 1.3.0
  • Provider/runtime: Claude Code
  • Unit: discuss-milestone

Reproduction Context

New project planning/discussion startup. The workflow requires these tools before writing planning artifacts:
ask_user_questions, gsd_summary_save, gsd_requirement_save, gsd_requirement_update, gsd_plan_milestone, gsd_milestone_generate_id.

Forensic Evidence

Duplicate check found related historical fixes, but none clearly cover this exact current false-negative path:

Current source still has a split-brain readiness path: SDK uses inline/phase MCP config, while preflight probes by persisted server name and drops probe errors.


Auto-generated by /gsd forensics

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageUnlabeled or unassigned — needs maintainer review

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions