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:
- Extend
awaitWorkflowMcpToolRegistration to accept a normalized workflowServerConfig or mcpServers map.
- In
stream-adapter.js, pass sdkMcpServers?.[workflowMcpServerName] ?? defaultMcpServers.servers[workflowMcpServerName] into preflight.
- In
tool-surface-readiness.js, call testMcpServerConnection(configObject, ...) when a config is supplied; only fall back to name lookup when no config is available.
- Track
lastError from the probe and append it to the timeout message, e.g. last probe error: Unknown MCP server or bridge startup stderr.
- 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
Problem
Claude Code can abort
discuss-milestonebefore the first model turn with: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
allowedToolsand callingawaitWorkflowMcpToolRegistration:awaitWorkflowMcpToolRegistrationthen probes by server name only, via persisted MCP config lookup, and discards the probe error:That name-based probe depends on
testMcpServerConnectionfinding a persisted MCP config entry:But the same Claude Code path builds/injects exact phase MCP server configs separately for the SDK:
So the preflight and the SDK session can validate different MCP surfaces: the SDK can be given an inline
gsd-workflowconfig 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:
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:
awaitWorkflowMcpToolRegistrationto accept a normalizedworkflowServerConfigormcpServersmap.stream-adapter.js, passsdkMcpServers?.[workflowMcpServerName] ?? defaultMcpServers.servers[workflowMcpServerName]into preflight.tool-surface-readiness.js, calltestMcpServerConnection(configObject, ...)when a config is supplied; only fall back to name lookup when no config is available.lastErrorfrom the probe and append it to the timeout message, e.g.last probe error: Unknown MCP serveror bridge startup stderr.autoInitClaudeCodeWorkflowMcp()return/throw an actionable error instead of silently swallowing failures unlessGSD_DEBUG=1.Suggested regression tests:
discuss-milestonephase with inlinemcpServersand no persisted.mcp.jsonshould pass preflight when tools/list returns required tools.did not register required tools before session startmessages.Environment
discuss-milestoneReproduction 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