Repository: gsd-build/gsd-2 (2026-04-10T20:44:37Z)

Issue:
[Feature]: Expose ask_user_questions through the packaged MCP server
## Summary
Expose ask_user_questions through @gsd-build/mcp-server so workflow MCP clients like Claude Code CLI can use structured user questions instead of failing compatibility checks.

## Problem to solve
The packaged MCP server already exposes the headless-safe workflow surface, but it still blocks guided flows that require ask_user_questions. That leaves the Claude Code CLI workflow transport unable to run interactive GSD units even when the client supports MCP elicitation. In local checkout flows, the spawned workflow server could also drift behind source changes because the transport preferred stale dist artifacts.

## Proposed solution
Add an MCP form-elicitation bridge for ask_user_questions in the packaged server, preserve the existing GSD answer JSON shape expected by prompts and write gates, add the tool to the workflow MCP compatibility surface, and make local workflow launches prefer live source modules and source CLI entrypoints when the TS resolve hook is available.

## Alternatives considered
Keeping ask_user_questions unavailable preserves a headless-only package surface, but that continues to block guided workflow units for elicitation-capable clients. Falling back to plain text prompts would bypass the structured answer contract GSD already depends on. Using form elicitation for secure_env_collect was also considered, but that would be the wrong security model for secrets.

## Use cases
- Running guided GSD milestone or slice discussions from Claude Code CLI over workflow MCP
- Allowing write-gated flows that depend on structured confirmation questions to proceed through the packaged server
- Keeping local checkout testing aligned with source changes instead of stale dist output during MCP workflow development

## Impact
Affected: users running GSD workflow MCP through Claude Code CLI or other elicitation-capable clients
Severity: medium
Frequency: whenever a guided workflow unit requires structured user input
Consequence: guided units fail capability checks or cannot collect the structured responses GSD expects

## Evidence or prior art
The in-process GSD extension already exposes ask_user_questions and existing prompts/write-gate logic expect its current JSON answer shape. The MCP SDK supports form elicitation, which is the appropriate transport for non-secret structured choices.

## Additional information
This change should keep secure_env_collect out of scope. That path should use MCP URL elicitation or another secure bridge rather than form elicitation.


Discussion:
This issue is tracked by PR #3946 (feat(mcp-server): expose ask_user_questions via elicitation), which is currently open and awaiting merge.

This issue will close automatically once that PR merges. You can follow progress there. Thanks for your patience!
CI is failing on linked PR #3946.

Hey @jeremymcs — the following checks are failing and will need to be resolved before this can merge:

- build — https://github.com/gsd-build/gsd-2/actions/runs/24263354533/job/70852473475

Once CI is green, this issue will be back on track. Let us know if you need help debugging!



Patch:
diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md
index dcc32aa943..f1544b8954 100644
--- a/packages/mcp-server/README.md
+++ b/packages/mcp-server/README.md
@@ -7,7 +7,8 @@ Start GSD auto-mode sessions, poll progress, resolve blockers, and retrieve resu
 This package now exposes two tool surfaces:
 
 - session/read tools for starting and inspecting GSD sessions
-- workflow mutation tools for planning, completion, validation, reassessment, and gate persistence
+- MCP-native interactive tools for structured user input
+- headless-safe workflow tools for planning, completion, validation, reassessment, metadata persistence, and journal reads
 
 ## Installation
 
@@ -74,18 +75,29 @@ Add to `.cursor/mcp.json`:
 
 ## Tools
 
-### Workflow mutation tools
+### Workflow tools
 
 The workflow MCP surface includes:
 
+- `gsd_decision_save`
+- `gsd_save_decision`
+- `gsd_requirement_update`
+- `gsd_update_requirement`
+- `gsd_requirement_save`
+- `gsd_save_requirement`
+- `gsd_milestone_generate_id`
+- `gsd_generate_milestone_id`
 - `gsd_plan_milestone`
 - `gsd_plan_slice`
+- `gsd_plan_task`
+- `gsd_task_plan`
 - `gsd_replan_slice`
 - `gsd_slice_replan`
 - `gsd_task_complete`
 - `gsd_complete_task`
 - `gsd_slice_complete`
 - `gsd_complete_slice`
+- `gsd_skip_slice`
 - `gsd_validate_milestone`
 - `gsd_milestone_validate`
 - `gsd_complete_milestone`
@@ -95,13 +107,21 @@ The workflow MCP surface includes:
 - `gsd_save_gate_result`
 - `gsd_summary_save`
 - `gsd_milestone_status`
+- `gsd_journal_query`
 
-These mutation tools use the same GSD workflow handlers as the native in-process tool path.
+These tools use the same GSD workflow handlers as the native in-process tool path wherever a shared handler exists.
+
+### Interactive tools
+
+The packaged server now exposes `ask_user_questions` through MCP form elicitation. This keeps the existing GSD answer payload shape while allowing Claude Code CLI and other elicitation-capable clients to surface structured user choices.
+
+`secure_env_collect` is still not exposed by this package. That path needs MCP URL elicitation or an equivalent secure bridge because secrets should not flow through form elicitation.
 
 Current support boundary:
 
 - when running inside the GSD monorepo checkout, the MCP server auto-discovers the shared workflow executor module
 - outside the monorepo, set `GSD_WORKFLOW_EXECUTORS_MODULE` to an importable `workflow-tool-executors` module path if you want the mutation tools enabled
+- `ask_user_questions` requires an MCP client that supports form elicitation
 - session/read tools do not depend on this bridge
 
 If the executor bridge cannot be loaded, workflow mutation calls will fail with a precise configuration error instead of silently degrading.
diff --git a/packages/mcp-server/src/cli.ts b/packages/mcp-server/src/cli.ts
index 744749d03a..141c4083fe 100644
--- a/packages/mcp-server/src/cli.ts
+++ b/packages/mcp-server/src/cli.ts
@@ -1,5 +1,3 @@
-#!/usr/bin/env node
-
 /**
  * @gsd-build/mcp-server CLI — stdio transport entry point.
  *
@@ -15,7 +13,8 @@ const MCP_PKG = '@modelcontextprotocol/sdk';
 async function main(): Promise<void> {
   const sessionManager = new SessionManager();
 
-  // Create the configured MCP server with all 12 tools (6 session + 6 read-only)
+  // Create the configured MCP server with session, interactive, read-only,
+  // and workflow tools.
   const { server } = await createMcpServer(sessionManager);
 
   // Dynamic import for StdioServerTransport (same TS subpath workaround)
diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts
index f4f5fe2062..1f969462ec 100644
--- a/packages/mcp-server/src/server.ts
+++ b/packages/mcp-server/src/server.ts
@@ -2,8 +2,9 @@
  * MCP Server — registers GSD orchestration, project-state, and workflow tools.
  *
  * Session tools (6): gsd_execute, gsd_status, gsd_result, gsd_cancel, gsd_query, gsd_resolve_blocker
+ * Interactive tools (1): ask_user_questions via MCP form elicitation
  * Read-only tools (6): gsd_progress, gsd_roadmap, gsd_history, gsd_doctor, gsd_captures, gsd_knowledge
- * Workflow tools (17): planning, replanning, completion, validation, reassessment, gate result, and milestone status tools
+ * Workflow tools (29): headless-safe planning, metadata persistence, replanning, completion, validation, reassessment, gate result, status, and journal tools
  *
  * Uses dynamic imports for @modelcontextprotocol/sdk because TS Node16
  * cannot resolve the SDK's subpath exports statically (same pattern as
@@ -44,6 +45,11 @@ function errorContent(message: string): { isError: true; content: Array<{ type:
   return { isError: true, content: [{ type: 'text' as const, text: message }] };
 }
 
+/** Return raw text content without JSON wrapping. */
+function textContent(text: string): { content: Array<{ type: 'text'; text: string }> } {
+  return { content: [{ type: 'text' as const, text }] };
+}
+
 // ---------------------------------------------------------------------------
 // gsd_query filesystem reader
 // ---------------------------------------------------------------------------
@@ -108,10 +114,155 @@ async function fileExists(path: string): Promise<boolean> {
 
 interface McpServerInstance {
   tool(name: string, description: string, params: Record<string, unknown>, handler: (args: Record<string, unknown>) => Promise<unknown>): unknown;
+  server: {
+    elicitInput(
+      params: AskUserQuestionsElicitRequest,
+      options?: unknown,
+    ): Promise<AskUserQuestionsElicitResult>;
+  };
   connect(transport: unknown): Promise<void>;
   close(): Promise<void>;
 }
 
+interface AskUserQuestionOption {
+  label: string;
+  description: string;
+}
+
+interface AskUserQuestion {
+  id: string;
+  header: string;
+  question: string;
+  options: AskUserQuestionOption[];
+  allowMultiple?: boolean;
+}
+
+interface AskUserQuestionsParams {
+  questions: AskUserQuestion[];
+}
+
+type AskUserQuestionsContentValue = string | number | boolean | string[];
+
+interface AskUserQuestionsElicitResult {
+  action: 'accept' | 'decline' | 'cancel';
+  content?: Record<string, AskUserQuestionsContentValue>;
+}
+
+interface AskUserQuestionsElicitRequest {
+  mode: 'form';
+  message: string;
+  requestedSchema: {
+    type: 'object';
+    properties: Record<string, Record<string, unknown>>;
+    required?: string[];
+  };
+}
+
+const OTHER_OPTION_LABEL = 'None of the above';
+
+function normalizeAskUserQuestionsNote(value: AskUserQuestionsContentValue | undefined): string {
+  return typeof value === 'string' ? value.trim() : '';
+}
+
+function normalizeAskUserQuestionsAnswers(
+  value: AskUserQuestionsContentValue | undefined,
+  allowMultiple: boolean,
+): string[] {
+  if (allowMultiple) {
+    return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
+  }
+
+  return typeof value === 'string' && value.length > 0 ? [value] : [];
+}
+
+function validateAskUserQuestionsPayload(questions: AskUserQuestion[]): string | null {
+  if (questions.length === 0 || questions.length > 3) {
+    return 'Error: questions must contain 1-3 items';
+  }
+
+  for (const question of questions) {
+    if (!question.options || question.options.length === 0) {
+      return `Error: ask_user_questions requires non-empty options for every question (question "${question.id}" has none)`;
+    }
+  }
+
+  return null;
+}
+
+export function buildAskUserQuestionsElicitRequest(questions: AskUserQuestion[]): AskUserQuestionsElicitRequest {
+  const properties: Record<string, Record<string, unknown>> = {};
+  const required = questions.map((question) => question.id);
+
+  for (const question of questions) {
+    if (question.allowMultiple) {
+      properties[question.id] = {
+        type: 'array',
+        title: question.header,
+        description: question.question,
+        minItems: 1,
+        maxItems: question.options.length,
+        items: {
+          anyOf: question.options.map((option) => ({
+            const: option.label,
+            title: option.label,
+          })),
+        },
+      };
+      continue;
+    }
+
+    properties[question.id] = {
+      type: 'string',
+      title: question.header,
+      description: question.question,
+      oneOf: [...question.options, { label: OTHER_OPTION_LABEL, description: 'Choose this when the listed options do not fit.' }].map((option) => ({
+        const: option.label,
+        title: option.label,
+      })),
+    };
+
+    properties[`${question.id}__note`] = {
+      type: 'string',
+      title: `${question.header} Note`,
+      description: `Optional note for "${OTHER_OPTION_LABEL}".`,
+      maxLength: 500,
+    };
+  }
+
+  return {
+    mode: 'form',
+    message: 'Please answer the following question(s). For single-select questions, choose "None of the above" and add a note if the provided options do not fit.',
+    requestedSchema: {
+      type: 'object',
+      properties,
+      required,
+    },
+  };
+}
+
+export function formatAskUserQuestionsElicitResult(
+  questions: AskUserQuestion[],
+  result: AskUserQuestionsElicitResult,
+): string {
+  const answers: Record<string, { answers: string[] }> = {};
+  const content = result.content ?? {};
+
+  for (const question of questions) {
+    const answerList = normalizeAskUserQuestionsAnswers(content[question.id], !!question.allowMultiple);
+
+    if (!question.allowMultiple && answerList[0] === OTHER_OPTION_LABEL) {
+      const note = normalizeAskUserQuestionsNote(content[`${question.id}__note`]);
+      if (note) {
+        answerList.push(`user_note: ${note}`);
+      }
+    }
+
+    answers[question.id] = { answers: answerList };
+  }
+
+  return JSON.stringify({ answers });
+}
+
 // ---------------------------------------------------------------------------
 // createMcpServer
 // ---------------------------------------------------------------------------
@@ -285,6 +436,42 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
     },
   );
 
+  // -----------------------------------------------------------------------
+  // ask_user_questions — structured user input via MCP form elicitation
+  // -----------------------------------------------------------------------
+  server.tool(
+    'ask_user_questions',
+    'Request user input for one to three short questions and wait for the response. Single-select questions include a free-form "None of the above" path. Multi-select questions allow multiple choices.',
+    {
+      questions: z.array(z.object({
+        id: z.string().describe('Stable identifier for mapping answers (snake_case)'),
+        header: z.string().describe('Short header label shown in the UI (12 or fewer chars)'),
+        question: z.string().describe('Single-sentence prompt shown to the user'),
+        options: z.array(z.object({
+          label: z.string().describe('User-facing label (1-5 words)'),
+          description: z.string().describe('One short sentence explaining impact/tradeoff if selected'),
+        })).describe('Provide 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with "(Recommended)". Do not include an "Other" option for single-select questions.'),
+        allowMultiple: z.boolean().optional().describe('If true, the user can select multiple options. No "None of the above" option is added.'),
+      })).describe('Questions to show the user. Prefer 1 and do not exceed 3.'),
+    },
+    async (args: Record<string, unknown>) => {
+      const { questions } = args as unknown as AskUserQuestionsParams;
+      try {
+        const validationError = validateAskUserQuestionsPayload(questions);
+        if (validationError) return errorContent(validationError);
+
+        const elicitation = await server.server.elicitInput(buildAskUserQuestionsElicitRequest(questions));
+        if (elicitation.action !== 'accept' || !elicitation.content) {
+          return textContent('ask_user_questions was cancelled before receiving a response');
+        }
+
+        return textContent(formatAskUserQuestionsElicitResult(questions, elicitation));
+      } catch (err) {
+        return errorContent(err instanceof Error ? err.message : String(err));
+      }
+    },
+  );
+
   // =======================================================================
   // READ-ONLY TOOLS — no session required, pure filesystem reads
   // =======================================================================
diff --git a/packages/mcp-server/src/workflow-tools.ts b/packages/mcp-server/src/workflow-tools.ts
index 95ea204943..ee3d1982b3 100644
--- a/packages/mcp-server/src/workflow-tools.ts
+++ b/packages/mcp-server/src/workflow-tools.ts
@@ -336,6 +336,10 @@ function toFileUrl(modulePath: string): string {
   return pathToFileURL(resolve(modulePath)).href;
 }
 
+async function importLocalModule<T>(relativePath: string): Promise<T> {
+  return import(new URL(relativePath, import.meta.url).href) as Promise<T>;
+}
+
 function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.env): string[] {
   const candidates: string[] = [];
   const explicitModule = env.GSD_WORKFLOW_EXECUTORS_MODULE?.trim();
@@ -420,6 +424,38 @@ interface McpToolServer {
   ): unknown;
 }
 
+export const WORKFLOW_TOOL_NAMES = [
+  "gsd_decision_save",
+  "gsd_save_decision",
+  "gsd_requirement_update",
+  "gsd_update_requirement",
+  "gsd_requirement_save",
+  "gsd_save_requirement",
+  "gsd_milestone_generate_id",
+  "gsd_generate_milestone_id",
+  "gsd_plan_milestone",
+  "gsd_plan_slice",
+  "gsd_plan_task",
+  "gsd_task_plan",
+  "gsd_replan_slice",
+  "gsd_slice_replan",
+  "gsd_slice_complete",
+  "gsd_complete_slice",
+  "gsd_skip_slice",
+  "gsd_complete_milestone",
+  "gsd_milestone_complete",
+  "gsd_validate_milestone",
+  "gsd_milestone_validate",
+  "gsd_reassess_roadmap",
+  "gsd_roadmap_reassess",
+  "gsd_save_gate_result",
+  "gsd_summary_save",
+  "gsd_task_complete",
+  "gsd_complete_task",
+  "gsd_milestone_status",
+  "gsd_journal_query",
+] as const;
+
 async function runSerializedWorkflowOperation<T>(fn: () => Promise<T>): Promise<T> {
   // The shared DB adapter and workflow log base path are process-global, so
   // workflow MCP mutations must not overlap within a single server process.
@@ -566,6 +602,15 @@ async function handleSaveGateResult(
   return runSerializedWorkflowOperation(() => executeSaveGateResult(params, projectDir));
 }
 
+async function ensureMilestoneDbRow(milestoneId: string): Promise<void> {
+  try {
+    const { insertMilestone } = await importLocalModule<any>("../../../src/resources/extensions/gsd/gsd-db.js");
+    insertMilestone({ id: milestoneId, status: "queued" });
+  } catch {
+    // Ignore pre-existing rows or transient DB availability issues.
+  }
+}
+
 const projectDirParam = z.string().describe("Absolute path to the project directory within the configured workflow root");
 
 const planMilestoneParams = {
@@ -772,6 +817,73 @@ const summarySaveParams = {
 };
 const summarySaveSchema = z.object(summarySaveParams);
 
+const decisionSaveParams = {
+  projectDir: projectDirParam,
+  scope: z.string().describe("Scope of the decision (e.g. architecture, library, observability)"),
+  decision: z.string().describe("What is being decided"),
+  choice: z.string().describe("The choice made"),
+  rationale: z.string().describe("Why this choice was made"),
+  revisable: z.string().optional().describe("Whether this can be revisited"),
+  when_context: z.string().optional().describe("When/context for the decision"),
+  made_by: z.enum(["human", "agent", "collaborative"]).optional().describe("Who made the decision"),
+};
+const decisionSaveSchema = z.object(decisionSaveParams);
+
+const requirementUpdateParams = {
+  projectDir: projectDirParam,
+  id: z.string().describe("Requirement ID (e.g. R001)"),
+  status: z.string().optional().describe("New status"),
+  validation: z.string().optional().describe("Validation criteria or proof"),
+  notes: z.string().optional().describe("Additional notes"),
+  description: z.string().optional().describe("Updated description"),
+  primary_owner: z.string().optional().describe("Primary owning slice"),
+  supporting_slices: z.string().optional().describe("Supporting slices"),
+};
+const requirementUpdateSchema = z.object(requirementUpdateParams);
+
+const requirementSaveParams = {
+  projectDir: projectDirParam,
+  class: z.string().describe("Requirement class"),
+  description: z.string().describe("Short description of the requirement"),
+  why: z.string().describe("Why this requirement matters"),
+  source: z.string().describe("Origin of the requirement"),
+  status: z.string().optional().describe("Requirement status"),
+  primary_owner: z.string().optional().describe("Primary owning slice"),
+  supporting_slices: z.string().optional().describe("Supporting slices"),
+  validation: z.string().optional().describe("Validation criteria"),
+  notes: z.string().optional().describe("Additional notes"),
+};
+const requirementSaveSchema = z.object(requirementSaveParams);
+
+const milestoneGenerateIdParams = {
+  projectDir: projectDirParam,
+};
+const milestoneGenerateIdSchema = z.object(milestoneGenerateIdParams);
+
+const planTaskParams = {
+  projectDir: projectDirParam,
+  milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
+  sliceId: z.string().describe("Slice ID (e.g. S01)"),
+  taskId: z.string().describe("Task ID (e.g. T01)"),
+  title: z.string().describe("Task title"),
+  description: z.string().describe("Task description / steps block"),
+  estimate: z.string().describe("Task estimate"),
+  files: z.array(z.string()).describe("Files likely touched"),
+  verify: z.string().describe("Verification command or block"),
+  inputs: z.array(z.string()).describe("Input files or references"),
+  expectedOutput: z.array(z.string()).describe("Expected output files or artifacts"),
+  observabilityImpact: z.string().optional().describe("Task observability impact"),
+};
+const planTaskSchema = z.object(planTaskParams);
+
+const skipSliceParams = {
+  projectDir: projectDirParam,
+  sliceId: z.string().describe("Slice ID (e.g. S02)"),
+  milestoneId: z.string().describe("Milestone ID (e.g. M003)"),
+  reason: z.string().optional().describe("Reason for skipping this slice"),
+};
+const skipSliceSchema = z.object(skipSliceParams);
+
 const taskCompleteParams = {
   projectDir: projectDirParam,
   taskId: z.string().describe("Task ID (e.g. T01)"),
@@ -803,7 +915,171 @@ const milestoneStatusParams = {
 };
 const milestoneStatusSchema = z.object(milestoneStatusParams);
 
+const journalQueryParams = {
+  projectDir: projectDirParam,
+  flowId: z.string().optional().describe("Filter by flow ID"),
+  unitId: z.string().optional().describe("Filter by unit ID"),
+  rule: z.string().optional().describe("Filter by rule name"),
+  eventType: z.string().optional().describe("Filter by event type"),
+  after: z.string().optional().describe("ISO-8601 lower bound (inclusive)"),
+  before: z.string().optional().describe("ISO-8601 upper bound (inclusive)"),
+  limit: z.number().optional().describe("Maximum entries to return"),
+};
+const journalQuerySchema = z.object(journalQueryParams);
+
 export function registerWorkflowTools(server: McpToolServer): void {
+  server.tool(
+    "gsd_decision_save",
+    "Record a project decision to the GSD database and regenerate DECISIONS.md.",
+    decisionSaveParams,
+    async (args: Record<string, unknown>) => {
+      const parsed = parseWorkflowArgs(decisionSaveSchema, args);
+      const { projectDir, ...params } = parsed;
+      await enforceWorkflowWriteGate("gsd_decision_save", projectDir);
+      const result = await runSerializedWorkflowOperation(async () => {
+        const { saveDecisionToDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
+        return saveDecisionToDb(params, projectDir);
+      });
+      return { content: [{ type: "text" as const, text: `Saved decision ${result.id}` }] };
+    },
+  );
+
+  server.tool(
+    "gsd_save_decision",
+    "Alias for gsd_decision_save. Record a project decision to the GSD database and regenerate DECISIONS.md.",
+    decisionSaveParams,
+    async (args: Record<string, unknown>) => {
+      const parsed = parseWorkflowArgs(decisionSaveSchema, args);
+      const { projectDir, ...params } = parsed;
+      await enforceWorkflowWriteGate("gsd_decision_save", projectDir);
+      const result = await runSerializedWorkflowOperation(async () => {
+        const { saveDecisionToDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
+        return saveDecisionToDb(params, projectDir);
+      });
+      return { content: [{ type: "text" as const, text: `Saved decision ${result.id}` }] };
+    },
+  );
+
+  server.tool(
+    "gsd_requirement_update",
+    "Update an existing requirement in the GSD database and regenerate REQUIREMENTS.md.",
+    requirementUpdateParams,
+    async (args: Record<string, unknown>) => {
+      const parsed = parseWorkflowArgs(requirementUpdateSchema, args);
+      const { projectDir, id, ...updates } = parsed;
+      await enforceWorkflowWriteGate("gsd_requirement_update", projectDir);
+      await runSerializedWorkflowOperation(async () => {
+        const { updateRequirementInDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
+        return updateRequirementInDb(id, updates, projectDir);
+      });
+      return { content: [{ type: "text" as const, text: `Updated requirement ${id}` }] };
+    },
+  );
+
+  server.tool(
+    "gsd_update_requirement",
+    "Alias for gsd_requirement_update. Update an existing requirement in the GSD database and regenerate REQUIREMENTS.md.",
+    requirementUpdateParams,
+    async (args: Record<string, unknown>) => {
+      const parsed = parseWorkflowArgs(requirementUpdateSchema, args);
+      const { projectDir, id, ...updates } = parsed;
+      await enforceWorkflowWriteGate("gsd_requirement_update", projectDir);
+      await runSerializedWorkflowOperation(async () => {
+        const { updateRequirementInDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
+        return updateRequirementInDb(id, updates, projectDir);
+      });
+      return { content: [{ type: "text" as const, text: `Updated requirement ${id}` }] };
+    },
+  );
+
+  server.tool(
+    "gsd_requirement_save",
+    "Record a new requirement to the GSD database and regenerate REQUIREMENTS.md.",
+    requirementSaveParams,
+    async (args: Record<string, unknown>) => {
+      const parsed = parseWorkflowArgs(requirementSaveSchema, args);
+      const { projectDir, ...params } = parsed;
+      await enforceWorkflowWriteGate("gsd_requirement_save", projectDir);
+      const result = await runSerializedWorkflowOperation(async () => {
+        const { saveRequirementToDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
+        return saveRequirementToDb(params, projectDir);
+      });
+      return { content: [{ type: "text" as const, text: `Saved requirement ${result.id}` }] };
+    },
+  );
+
+  server.tool(
+    "gsd_save_requirement",
+    "Alias for gsd_requirement_save. Record a new requirement to the GSD database and regenerate REQUIREMENTS.md.",
+    requirementSaveParams,
+    async (args: Record<string, unknown>) => {
+      const parsed = parseWorkflowArgs(requirementSaveSchema, args);
+      const { projectDir, ...params } = parsed;
+      await enforceWorkflowWriteGate("gsd_requirement_save", projectDir);
+      const result = await runSerializedWorkflowOperation(async () => {
+        const { saveRequirementToDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
+        return saveRequirementToDb(params, projectDir);
+      });
+      return { content: [{ type: "text" as const, text: `Saved requirement ${result.id}` }] };
+    },
+  );
+
+  server.tool(
+    "gsd_milestone_generate_id",
+    "Generate the next milestone ID for a new GSD milestone.",
+    milestoneGenerateIdParams,
+    async (args: Record<string, unknown>) => {
+      const { projectDir } = parseWorkflowArgs(milestoneGenerateIdSchema, args);
+      await enforceWorkflowWriteGate("gsd_milestone_generate_id", projectDir);
+      const id = await runSerializedWorkflowOperation(async () => {
+        const {
+          claimReservedId,
+          findMilestoneIds,
+          getReservedMilestoneIds,
+          nextMilestoneId,
+        } = await importLocalModule<any>("../../../src/resources/extensions/gsd/milestone-ids.js");
+        const reserved = claimReservedId();
+        if (reserved) {
+          await ensureMilestoneDbRow(reserved);
+          return reserved;
+        }
+        const allIds = [...new Set([...findMilestoneIds(projectDir), ...getReservedMilestoneIds()])];
+        const nextId = nextMilestoneId(allIds);
+        await ensureMilestoneDbRow(nextId);
+        return nextId;
+      });
+      return { content: [{ type: "text" as const, text: id }] };
+    },
+  );
+
+  server.tool(
+    "gsd_generate_milestone_id",
+    "Alias for gsd_milestone_generate_id. Generate the next milestone ID for a new GSD milestone.",
+    milestoneGenerateIdParams,
+    async (args: Record<string, unknown>) => {
+      const { projectDir } = parseWorkflowArgs(milestoneGenerateIdSchema, args);
+      await enforceWorkflowWriteGate("gsd_milestone_generate_id", projectDir);
+      const id = await runSerializedWorkflowOperation(async () => {
+        const {
+          claimReservedId,
+          findMilestoneIds,
+          getReservedMilestoneIds,
+          nextMilestoneId,
+        } = await importLocalModule<any>("../../../src/resources/extensions/gsd/milestone-ids.js");
+        const reserved = claimReservedId();
+        if (reserved) {
+          await ensureMilestoneDbRow(reserved);
+          return reserved;
+        }
+        const allIds = [...new Set([...findMilestoneIds(projectDir), ...getReservedMilestoneIds()])];
+        const nextId = nextMilestoneId(allIds);
+        await ensureMilestoneDbRow(nextId);
+        return nextId;
+      });
+      return { content: [{ type: "text" as const, text: id }] };
+    },
+  );
+
   server.tool(
     "gsd_plan_milestone",
     "Write milestone planning state to the GSD database and render ROADMAP.md from DB.",
@@ -830,6 +1106,48 @@ export function registerWorkflowTools(server: McpToolServer): void {
     },
   );
 
+  server.tool(
+    "gsd_plan_task",
+    "Write task planning state to the GSD database and render tasks/T##-PLAN.md from DB.",
+    planTaskParams,
+    async (args: Record<string, unknown>) => {
+      const parsed = parseWorkflowArgs(planTaskSchema, args);
+      const { projectDir, ...params } = parsed;
+      await enforceWorkflowWriteGate("gsd_plan_task", projectDir, params.milestoneId);
+      const result = await runSerializedWorkflowOperation(async () => {
+        const { handlePlanTask } = await importLocalModule<any>("../../../src/resources/extensions/gsd/tools/plan-task.js");
+        return handlePlanTask(params, projectDir);
+      });
+      if ("error" in result) {
+        throw new Error(result.error);
+      }
+      return {
+        content: [{ type: "text" as const, text: `Planned task ${result.taskId} (${result.sliceId}/${result.milestoneId})` }],
+      };
+    },
+  );
+
+  server.tool(
+    "gsd_task_plan",
+    "Alias for gsd_plan_task. Write task planning state to the GSD database and render tasks/T##-PLAN.md from DB.",
+    planTaskParams,
+    async (args: Record<string, unknown>) => {
+      const parsed = parseWorkflowArgs(planTaskSchema, args);
+      const { projectDir, ...params } = parsed;
+      await enforceWorkflowWriteGate("gsd_plan_task", projectDir, params.milestoneId);
+      const result = await runSerializedWorkflowOperation(async () => {
+        const { handlePlanTask } = await importLocalModule<any>("../../../src/resources/extensions/gsd/tools/plan-task.js");
+        return handlePlanTask(params, projectDir);
+      });
+      if ("error" in result) {
+        throw new Error(result.error);
+      }
+      return {
+        content: [{ type: "text" as const, text: `Planned task ${result.taskId} (${result.sliceId}/${result.milestoneId})` }],
+      };
+    },
+  );
+
   server.tool(
     "gsd_replan_slice",
     "Replan a slice after a blocker is discovered, preserving completed tasks and re-rendering PLAN.md + REPLAN.md.",
@@ -870,6 +1188,36 @@ export function registerWorkflowTools(server: McpToolServer): void {
     },
   );
 
+  server.tool(
+    "gsd_skip_slice",
+    "Mark a slice as skipped so auto-mode advances past it without executing.",
+    skipSliceParams,
+    async (args: Record<string, unknown>) => {
+      const { projectDir, milestoneId, sliceId, reason } = parseWorkflowArgs(skipSliceSchema, args);
+      await enforceWorkflowWriteGate("gsd_skip_slice", projectDir, milestoneId);
+      await runSerializedWorkflowOperation(async () => {
+        const { getSlice, updateSliceStatus } = await importLocalModule<any>("../../../src/resources/extensions/gsd/gsd-db.js");
+        const { invalidateStateCache } = await importLocalModule<any>("../../../src/resources/extensions/gsd/state.js");
+        const { rebuildState } = await importLocalModule<any>("../../../src/resources/extensions/gsd/doctor.js");
+        const slice = getSlice(milestoneId, sliceId);
+        if (!slice) {
+          throw new Error(`Slice ${sliceId} not found in milestone ${milestoneId}`);
+        }
+        if (slice.status === "complete" || slice.status === "done") {
+          throw new Error(`Slice ${sliceId} is already complete and cannot be skipped`);
+        }
+        if (slice.status !== "skipped") {
+          updateSliceStatus(milestoneId, sliceId, "skipped");
+          invalidateStateCache();
+          await rebuildState(projectDir);
+        }
+      });
+      return {
+        content: [{ type: "text" as const, text: `Skipped slice ${sliceId} (${milestoneId}). Reason: ${reason ?? "User-directed skip"}.` }],
+      };
+    },
+  );
+
   server.tool(
     "gsd_complete_milestone",
     "Record a completed milestone to the GSD database and render its SUMMARY.md.",
@@ -994,4 +1342,19 @@ export function registerWorkflowTools(server: McpToolServer): void {
       return runSerializedWorkflowOperation(() => executeMilestoneStatus({ milestoneId }, projectDir));
     },
   );
+
+  server.tool(
+    "gsd_journal_query",
+    "Query the structured event journal for auto-mode iterations.",
+    journalQueryParams,
+    async (args: Record<string, unknown>) => {
+      const { projectDir, limit, ...filters } = parseWorkflowArgs(journalQuerySchema, args);
+      const { queryJournal } = await importLocalModule<any>("../../../src/resources/extensions/gsd/journal.js");
+      const entries = queryJournal(projectDir, filters).slice(0, limit ?? 100);
+      if (entries.length === 0) {
+        return { content: [{ type: "text" as const, text: "No matching journal entries found." }] };
+      }
+      return { content: [{ type: "text" as const, text: JSON.stringify(entries, null, 2) }] };
+    },
+  );
 }
diff --git a/src/resources/extensions/gsd/workflow-mcp.ts b/src/resources/extensions/gsd/workflow-mcp.ts
index 797f127f57..809aa15435 100644
--- a/src/resources/extensions/gsd/workflow-mcp.ts
+++ b/src/resources/extensions/gsd/workflow-mcp.ts
@@ -1,7 +1,7 @@
 import { execSync } from "node:child_process";
 import { existsSync } from "node:fs";
 import { dirname, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
+import { fileURLToPath, pathToFileURL } from "node:url";
 
 export interface WorkflowMcpLaunchConfig {
   name: string;
@@ -21,22 +21,35 @@ export interface WorkflowCapabilityOptions {
 }
 
 const MCP_WORKFLOW_TOOL_SURFACE = new Set([
+  "ask_user_questions",
+  "gsd_decision_save",
   "gsd_complete_milestone",
   "gsd_complete_task",
   "gsd_complete_slice",
+  "gsd_generate_milestone_id",
+  "gsd_journal_query",
   "gsd_milestone_complete",
+  "gsd_milestone_generate_id",
   "gsd_milestone_status",
   "gsd_milestone_validate",
+  "gsd_plan_task",
   "gsd_plan_milestone",
   "gsd_plan_slice",
   "gsd_replan_slice",
   "gsd_reassess_roadmap",
+  "gsd_requirement_save",
+  "gsd_requirement_update",
   "gsd_roadmap_reassess",
+  "gsd_save_decision",
   "gsd_save_gate_result",
+  "gsd_save_requirement",
+  "gsd_skip_slice",
   "gsd_slice_replan",
   "gsd_slice_complete",
   "gsd_summary_save",
+  "gsd_task_plan",
   "gsd_task_complete",
+  "gsd_update_requirement",
   "gsd_validate_milestone",
 ]);
 
@@ -95,6 +108,8 @@ function getBundledWorkflowMcpCliPath(env: NodeJS.ProcessEnv): string | null {
   }
 
   const candidates = [
+    resolve(fileURLToPath(new URL("../../../../packages/mcp-server/src/cli.ts", import.meta.url))),
+    resolve(fileURLToPath(new URL("../../../../../packages/mcp-server/src/cli.ts", import.meta.url))),
     resolve(fileURLToPath(new URL("../../../../packages/mcp-server/dist/cli.js", import.meta.url))),
     resolve(fileURLToPath(new URL("../../../../../packages/mcp-server/dist/cli.js", import.meta.url))),
   ];
@@ -108,9 +123,9 @@ function getBundledWorkflowMcpCliPath(env: NodeJS.ProcessEnv): string | null {
 
 function getBundledWorkflowExecutorModulePath(): string | null {
   const candidates = [
-    resolve(fileURLToPath(new URL("../../../../dist/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url))),
     resolve(fileURLToPath(new URL("./tools/workflow-tool-executors.js", import.meta.url))),
     resolve(fileURLToPath(new URL("./tools/workflow-tool-executors.ts", import.meta.url))),
+    resolve(fileURLToPath(new URL("../../../../dist/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url))),
   ];
 
   for (const candidate of candidates) {
@@ -122,9 +137,9 @@ function getBundledWorkflowExecutorModulePath(): string | null {
 
 function getBundledWorkflowWriteGateModulePath(): string | null {
   const candidates = [
-    resolve(fileURLToPath(new URL("../../../../dist/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url))),
     resolve(fileURLToPath(new URL("./bootstrap/write-gate.js", import.meta.url))),
     resolve(fileURLToPath(new URL("./bootstrap/write-gate.ts", import.meta.url))),
+    resolve(fileURLToPath(new URL("../../../../dist/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url))),
   ];
 
   for (const candidate of candidates) {
@@ -134,19 +149,58 @@ function getBundledWorkflowWriteGateModulePath(): string | null {
   return null;
 }
 
+function getResolveTsHookPath(): string | null {
+  const candidates = [
+    resolve(fileURLToPath(new URL("./tests/resolve-ts.mjs", import.meta.url))),
+    resolve(fileURLToPath(new URL("../../../../src/resources/extensions/gsd/tests/resolve-ts.mjs", import.meta.url))),
+  ];
+
+  for (const candidate of candidates) {
+    if (existsSync(candidate)) return candidate;
+  }
+
+  return null;
+}
+
+function mergeNodeOptions(existing: string | undefined, additions: string[]): string | undefined {
+  const tokens = (existing ?? "").split(/\s+/).map((value) => value.trim()).filter(Boolean);
+  for (const addition of additions) {
+    if (!tokens.includes(addition)) {
+      tokens.push(addition);
+    }
+  }
+  return tokens.length > 0 ? tokens.join(" ") : undefined;
+}
+
 function buildWorkflowLaunchEnv(
   projectRoot: string,
   gsdCliPath: string | undefined,
   explicitEnv?: Record<string, string>,
+  workflowCliPath?: string,
 ): Record<string, string> {
   const executorModulePath = getBundledWorkflowExecutorModulePath();
   const writeGateModulePath = getBundledWorkflowWriteGateModulePath();
+  const resolveTsHookPath = getResolveTsHookPath();
+  const wantsSourceTs =
+    Boolean(resolveTsHookPath) &&
+    (
+      (workflowCliPath?.endsWith(".ts") ?? false) ||
+      (executorModulePath?.endsWith(".ts") ?? false) ||
+      (writeGateModulePath?.endsWith(".ts") ?? false)
+    );
+  const nodeOptions = wantsSourceTs
+    ? mergeNodeOptions(explicitEnv?.NODE_OPTIONS, [
+        "--experimental-strip-types",
+        `--import=${pathToFileURL(resolveTsHookPath!).href}`,
+      ])
+    : explicitEnv?.NODE_OPTIONS;
 
   return {
     ...(explicitEnv ?? {}),
     ...(gsdCliPath ? { GSD_CLI_PATH: gsdCliPath } : {}),
     ...(executorModulePath ? { GSD_WORKFLOW_EXECUTORS_MODULE: executorModulePath } : {}),
     ...(writeGateModulePath ? { GSD_WORKFLOW_WRITE_GATE_MODULE: writeGateModulePath } : {}),
+    ...(nodeOptions ? { NODE_OPTIONS: nodeOptions } : {}),
     GSD_PERSIST_WRITE_GATE_STATE: "1",
     GSD_WORKFLOW_PROJECT_ROOT: projectRoot,
   };
@@ -188,7 +242,7 @@ export function detectWorkflowMcpLaunchConfig(
       command: process.execPath,
       args: [distCli],
       cwd: resolvedWorkflowProjectRoot,
-      env: buildWorkflowLaunchEnv(resolvedWorkflowProjectRoot, gsdCliPath),
+      env: buildWorkflowLaunchEnv(resolvedWorkflowProjectRoot, gsdCliPath, undefined, distCli),
     };
   }
 
@@ -199,7 +253,7 @@ export function detectWorkflowMcpLaunchConfig(
       command: process.execPath,
       args: [bundledCli],
       cwd: resolvedWorkflowProjectRoot,
-      env: buildWorkflowLaunchEnv(resolvedWorkflowProjectRoot, gsdCliPath),
+      env: buildWorkflowLaunchEnv(resolvedWorkflowProjectRoot, gsdCliPath, undefined, bundledCli),
     };
   }
 


Test patch:
diff --git a/packages/mcp-server/src/mcp-server.test.ts b/packages/mcp-server/src/mcp-server.test.ts
index 6d7ce156ed..c3ba68065b 100644
--- a/packages/mcp-server/src/mcp-server.test.ts
+++ b/packages/mcp-server/src/mcp-server.test.ts
@@ -16,7 +16,11 @@ import { resolve } from 'node:path';
 import { EventEmitter } from 'node:events';
 
 import { SessionManager } from './session-manager.js';
-import { createMcpServer } from './server.js';
+import {
+  buildAskUserQuestionsElicitRequest,
+  createMcpServer,
+  formatAskUserQuestionsElicitResult,
+} from './server.js';
 import { MAX_EVENTS } from './types.js';
 import type { ManagedSession, CostAccumulator, PendingBlocker } from './types.js';
 
@@ -574,6 +578,8 @@ describe('createMcpServer tool registration', () => {
   it('creates server successfully with all required methods', async () => {
     const { server } = await createMcpServer(sm);
     assert.ok(server);
+    assert.ok(server.server);
+    assert.equal(typeof server.server.elicitInput, 'function');
     assert.ok(typeof server.connect === 'function');
     assert.ok(typeof server.close === 'function');
   });
@@ -625,4 +631,82 @@ describe('createMcpServer tool registration', () => {
     const session = sm.getSession(sessionId)!;
     assert.equal(session.status, 'cancelled');
   });
+
+  it('buildAskUserQuestionsElicitRequest adds None of the above note field for single-select questions', () => {
+    const request = buildAskUserQuestionsElicitRequest([
+      {
+        id: 'depth_verification_M001',
+        header: 'Depth Check',
+        question: 'Did I capture the depth right?',
+        options: [
+          { label: 'Yes, you got it (Recommended)', description: 'Continue with the current summary.' },
+          { label: 'Not quite', description: 'I need to clarify the depth further.' },
+        ],
+      },
+      {
+        id: 'focus_areas',
+        header: 'Focus',
+        question: 'Which areas matter most?',
+        allowMultiple: true,
+        options: [
+          { label: 'Frontend', description: 'Prioritize the UI.' },
+          { label: 'Backend', description: 'Prioritize server logic.' },
+        ],
+      },
+    ]);
+
+    assert.equal(request.mode, 'form');
+    assert.deepEqual(request.requestedSchema.required, ['depth_verification_M001', 'focus_areas']);
+    assert.ok(request.requestedSchema.properties['depth_verification_M001']);
+    assert.ok(request.requestedSchema.properties['depth_verification_M001__note']);
+    assert.ok(!request.requestedSchema.properties['focus_areas__note']);
+  });
+
+  it('formatAskUserQuestionsElicitResult preserves the existing answers JSON shape', () => {
+    const result = formatAskUserQuestionsElicitResult(
+      [
+        {
+          id: 'depth_verification_M001',
+          header: 'Depth Check',
+          question: 'Did I capture the depth right?',
+          options: [
+            { label: 'Yes, you got it (Recommended)', description: 'Continue with the current summary.' },
+            { label: 'Not quite', description: 'I need to clarify the depth further.' },
+          ],
+        },
+        {
+          id: 'focus_areas',
+          header: 'Focus',
+          question: 'Which areas matter most?',
+          allowMultiple: true,
+          options: [
+            { label: 'Frontend', description: 'Prioritize the UI.' },
+            { label: 'Backend', description: 'Prioritize server logic.' },
+          ],
+        },
+      ],
+      {
+        action: 'accept',
+        content: {
+          depth_verification_M001: 'None of the above',
+          depth_verification_M001__note: 'Need more implementation detail.',
+          focus_areas: ['Frontend', 'Backend'],
+        },
+      },
+    );
+
+    assert.equal(
+      result,
+      JSON.stringify({
+        answers: {
+          depth_verification_M001: {
+            answers: ['None of the above', 'user_note: Need more implementation detail.'],
+          },
+          focus_areas: {
+            answers: ['Frontend', 'Backend'],
+          },
+        },
+      }),
+    );
+  });
 });
diff --git a/packages/mcp-server/src/workflow-tools.test.ts b/packages/mcp-server/src/workflow-tools.test.ts
index 35a883b3b9..d671a0a0a7 100644
--- a/packages/mcp-server/src/workflow-tools.test.ts
+++ b/packages/mcp-server/src/workflow-tools.test.ts
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
 import { randomUUID } from "node:crypto";
 
 import { _getAdapter, closeDatabase } from "../../../src/resources/extensions/gsd/gsd-db.ts";
-import { registerWorkflowTools } from "./workflow-tools.ts";
+import { registerWorkflowTools, WORKFLOW_TOOL_NAMES } from "./workflow-tools.ts";
 
 function makeTmpBase(): string {
   const base = join(tmpdir(), `gsd-mcp-workflow-${randomUUID()}`);
@@ -68,33 +68,12 @@ function makeMockServer() {
 }
 
 describe("workflow MCP tools", () => {
-  it("registers the seventeen workflow tools", () => {
+  it("registers the full headless-safe workflow tool surface", () => {
     const server = makeMockServer();
     registerWorkflowTools(server as any);
 
-    assert.equal(server.tools.length, 17);
-    assert.deepEqual(
-      server.tools.map((t) => t.name),
-      [
-        "gsd_plan_milestone",
-        "gsd_plan_slice",
-        "gsd_replan_slice",
-        "gsd_slice_replan",
-        "gsd_slice_complete",
-        "gsd_complete_slice",
-        "gsd_complete_milestone",
-        "gsd_milestone_complete",
-        "gsd_validate_milestone",
-        "gsd_milestone_validate",
-        "gsd_reassess_roadmap",
-        "gsd_roadmap_reassess",
-        "gsd_save_gate_result",
-        "gsd_summary_save",
-        "gsd_task_complete",
-        "gsd_complete_task",
-        "gsd_milestone_status",
-      ],
-    );
+    assert.equal(server.tools.length, WORKFLOW_TOOL_NAMES.length);
+    assert.deepEqual(server.tools.map((t) => t.name), [...WORKFLOW_TOOL_NAMES]);
   });
 
   it("gsd_summary_save writes artifact through the shared executor", async () => {
diff --git a/src/resources/extensions/gsd/tests/mcp-project-config.test.ts b/src/resources/extensions/gsd/tests/mcp-project-config.test.ts
index 0c2cdba5c2..7638a7e745 100644
--- a/src/resources/extensions/gsd/tests/mcp-project-config.test.ts
+++ b/src/resources/extensions/gsd/tests/mcp-project-config.test.ts
@@ -26,8 +26,12 @@ test("ensureProjectWorkflowMcpConfig creates .mcp.json with the workflow server"
     assert.equal(typeof server?.command, "string");
     assert.equal(Array.isArray(server?.args), true);
     assert.equal(server?.env?.GSD_WORKFLOW_PROJECT_ROOT, projectRoot);
-    assert.match(server?.env?.GSD_WORKFLOW_EXECUTORS_MODULE ?? "", /workflow-tool-executors\.js$/);
-    assert.match(server?.env?.GSD_WORKFLOW_WRITE_GATE_MODULE ?? "", /write-gate\.js$/);
+    assert.match(server?.env?.GSD_WORKFLOW_EXECUTORS_MODULE ?? "", /workflow-tool-executors\.(js|ts)$/);
+    assert.match(server?.env?.GSD_WORKFLOW_WRITE_GATE_MODULE ?? "", /write-gate\.(js|ts)$/);
+    if ((server?.env?.GSD_WORKFLOW_EXECUTORS_MODULE ?? "").endsWith(".ts")) {
+      assert.match(server?.env?.NODE_OPTIONS ?? "", /--experimental-strip-types/);
+      assert.match(server?.env?.NODE_OPTIONS ?? "", /resolve-ts\.mjs/);
+    }
   } finally {
     rmSync(projectRoot, { recursive: true, force: true });
   }
diff --git a/src/resources/extensions/gsd/tests/workflow-mcp.test.ts b/src/resources/extensions/gsd/tests/workflow-mcp.test.ts
index fb91a1b94e..8e05750963 100644
--- a/src/resources/extensions/gsd/tests/workflow-mcp.test.ts
+++ b/src/resources/extensions/gsd/tests/workflow-mcp.test.ts
@@ -141,7 +141,11 @@ test("detectWorkflowMcpLaunchConfig resolves the bundled server relative to the
   assert.match(launch?.env?.GSD_WORKFLOW_EXECUTORS_MODULE ?? "", /workflow-tool-executors\.(js|ts)$/);
   assert.match(launch?.env?.GSD_WORKFLOW_WRITE_GATE_MODULE ?? "", /write-gate\.(js|ts)$/);
   assert.equal(typeof launch?.args?.[0], "string");
-  assert.match(launch?.args?.[0] ?? "", /packages[\/\\]mcp-server[\/\\]dist[\/\\]cli\.js$/);
+  assert.match(launch?.args?.[0] ?? "", /packages[\/\\]mcp-server[\/\\](dist[\/\\]cli\.js|src[\/\\]cli\.ts)$/);
+  if ((launch?.args?.[0] ?? "").endsWith(".ts")) {
+    assert.match(launch?.env?.NODE_OPTIONS ?? "", /--experimental-strip-types/);
+    assert.match(launch?.env?.NODE_OPTIONS ?? "", /resolve-ts\.mjs/);
+  }
 });
 
 test("detectWorkflowMcpLaunchConfig resolves the bundled server relative to the package without env hints", () => {
@@ -154,7 +158,11 @@ test("detectWorkflowMcpLaunchConfig resolves the bundled server relative to the
   assert.match(launch?.env?.GSD_WORKFLOW_EXECUTORS_MODULE ?? "", /workflow-tool-executors\.(js|ts)$/);
   assert.match(launch?.env?.GSD_WORKFLOW_WRITE_GATE_MODULE ?? "", /write-gate\.(js|ts)$/);
   assert.equal(typeof launch?.args?.[0], "string");
-  assert.match(launch?.args?.[0] ?? "", /packages[\/\\]mcp-server[\/\\]dist[\/\\]cli\.js$/);
+  assert.match(launch?.args?.[0] ?? "", /packages[\/\\]mcp-server[\/\\](dist[\/\\]cli\.js|src[\/\\]cli\.ts)$/);
+  if ((launch?.args?.[0] ?? "").endsWith(".ts")) {
+    assert.match(launch?.env?.NODE_OPTIONS ?? "", /--experimental-strip-types/);
+    assert.match(launch?.env?.NODE_OPTIONS ?? "", /resolve-ts\.mjs/);
+  }
 });
 
 test("workflow MCP launch config reaches mutation tools over stdio", async () => {
@@ -165,12 +173,16 @@ test("workflow MCP launch config reaches mutation tools over stdio", async () =>
   assert.ok(launch, "expected a workflow MCP launch config");
   assert.match(
     launch.env?.GSD_WORKFLOW_EXECUTORS_MODULE ?? "",
-    /dist[\/\\]resources[\/\\]extensions[\/\\]gsd[\/\\]tools[\/\\]workflow-tool-executors\.js$/,
+    /(dist[\/\\]resources[\/\\]extensions[\/\\]gsd[\/\\]tools[\/\\]workflow-tool-executors\.js|src[\/\\]resources[\/\\]extensions[\/\\]gsd[\/\\]tools[\/\\]workflow-tool-executors\.(js|ts))$/,
   );
   assert.match(
     launch.env?.GSD_WORKFLOW_WRITE_GATE_MODULE ?? "",
-    /dist[\/\\]resources[\/\\]extensions[\/\\]gsd[\/\\]bootstrap[\/\\]write-gate\.js$/,
+    /(dist[\/\\]resources[\/\\]extensions[\/\\]gsd[\/\\]bootstrap[\/\\]write-gate\.js|src[\/\\]resources[\/\\]extensions[\/\\]gsd[\/\\]bootstrap[\/\\]write-gate\.(js|ts))$/,
   );
+  if ((launch.env?.GSD_WORKFLOW_EXECUTORS_MODULE ?? "").endsWith(".ts")) {
+    assert.match(launch.env?.NODE_OPTIONS ?? "", /--experimental-strip-types/);
+    assert.match(launch.env?.NODE_OPTIONS ?? "", /resolve-ts\.mjs/);
+  }
 
   const client = new Client({ name: "workflow-mcp-transport-test", version: "1.0.0" });
   const transport = new StdioClientTransport({
@@ -189,6 +201,10 @@ test("workflow MCP launch config reaches mutation tools over stdio", async () =>
       (tools.tools ?? []).some((tool) => tool.name === "gsd_plan_slice"),
       "expected workflow MCP surface to expose gsd_plan_slice",
     );
+    assert.ok(
+      (tools.tools ?? []).some((tool) => tool.name === "ask_user_questions"),
+      "expected workflow MCP surface to expose ask_user_questions",
+    );
 
     const milestoneResult = await client.callTool(
       {
@@ -465,18 +481,18 @@ test("transport compatibility now allows replan-slice over workflow MCP surface"
 test("transport compatibility still blocks units whose MCP tools are not exposed", () => {
   const error = getWorkflowTransportSupportError(
     "claude-code",
-    ["gsd_skip_slice"],
+    ["secure_env_collect"],
     {
       projectRoot: "/tmp/project",
       env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
       surface: "auto-mode",
-      unitType: "skip-slice",
+      unitType: "guided-discussion",
       authMode: "externalCli",
       baseUrl: "local://claude-code",
     },
   );
 
-  assert.match(error ?? "", /requires gsd_skip_slice/);
+  assert.match(error ?? "", /requires secure_env_collect/);
   assert.match(error ?? "", /currently exposes only/);
 });
 
diff --git a/src/tests/package-mcp-server-elicitation.test.ts b/src/tests/package-mcp-server-elicitation.test.ts
new file mode 100644
index 0000000000..a746d80941
--- /dev/null
+++ b/src/tests/package-mcp-server-elicitation.test.ts
@@ -0,0 +1,227 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { Client } from '@modelcontextprotocol/sdk/client/index.js'
+import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
+import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js'
+
+import {
+  buildAskUserQuestionsElicitRequest,
+  createMcpServer,
+  formatAskUserQuestionsElicitResult,
+} from '../../packages/mcp-server/src/server.js'
+
+function createSessionManagerStub() {
+  return {
+    startSession: async () => {
+      throw new Error('not implemented in test')
+    },
+    getSession: () => undefined,
+    getResult: () => undefined,
+    cancelSession: async () => {},
+    resolveBlocker: async () => {},
+  }
+}
+
+async function createConnectedClient(options?: {
+  onElicit?: (params: unknown) => Promise<unknown>,
+}) {
+  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
+
+  const { server } = await createMcpServer(createSessionManagerStub() as never)
+  const client = new Client({
+    name: 'test-client',
+    version: '0.0.0',
+  }, {
+    capabilities: {
+      elicitation: {},
+    },
+  })
+
+  if (options?.onElicit) {
+    client.setRequestHandler(ElicitRequestSchema, options.onElicit)
+  }
+
+  await Promise.all([
+    server.connect(serverTransport),
+    client.connect(clientTransport),
+  ])
+
+  return {
+    client,
+    close: async () => {
+      await client.close()
+      await server.close()
+    },
+  }
+}
+
+test('package MCP server exposes ask_user_questions over listTools', async () => {
+  const { client, close } = await createConnectedClient()
+
+  try {
+    const tools = await client.listTools()
+    assert.ok(tools.tools.some(tool => tool.name === 'ask_user_questions'))
+  } finally {
+    await close()
+  }
+})
+
+test('ask_user_questions returns the packaged answers JSON shape for form elicitation', async () => {
+  const { client, close } = await createConnectedClient({
+    onElicit: async (request) => {
+      const elicitation = (request as {
+        params?: {
+          message: string,
+          requestedSchema: { properties: Record<string, unknown>, required?: string[] },
+        },
+      }).params ?? request as {
+        message: string,
+        requestedSchema: { properties: Record<string, unknown>, required?: string[] },
+      }
+      assert.match(elicitation.message, /Please answer the following question/)
+      assert.ok(elicitation.requestedSchema.properties.deployment)
+      assert.ok(elicitation.requestedSchema.properties['deployment__note'])
+      assert.ok(elicitation.requestedSchema.required?.includes('deployment'))
+
+      return {
+        action: 'accept',
+        content: {
+          deployment: 'None of the above',
+          deployment__note: 'Need hybrid deployment.',
+        },
+      }
+    },
+  })
+
+  try {
+    const result = await client.callTool({
+      name: 'ask_user_questions',
+      arguments: {
+        questions: [
+          {
+            id: 'deployment',
+            header: 'Deploy',
+            question: 'Where will this run?',
+            options: [
+              { label: 'Cloud', description: 'Managed hosting.' },
+              { label: 'On-prem', description: 'Runs in customer infrastructure.' },
+            ],
+          },
+        ],
+      },
+    })
+
+    const text = result.content.find(item => item.type === 'text')
+    assert.ok(text && 'text' in text)
+    assert.equal(
+      text.text,
+      JSON.stringify({
+        answers: {
+          deployment: {
+            answers: ['None of the above', 'user_note: Need hybrid deployment.'],
+          },
+        },
+      }),
+    )
+  } finally {
+    await close()
+  }
+})
+
+test('ask_user_questions returns an error result for invalid question payloads', async () => {
+  const { client, close } = await createConnectedClient()
+
+  try {
+    const result = await client.callTool({
+      name: 'ask_user_questions',
+      arguments: {
+        questions: [
+          {
+            id: 'broken',
+            header: 'Broken',
+            question: 'This payload is invalid',
+            options: [],
+          },
+        ],
+      },
+    })
+
+    const text = result.content.find(item => item.type === 'text')
+    assert.ok(text && 'text' in text)
+    assert.equal(result.isError, true)
+    assert.match(text.text, /requires non-empty options/i)
+  } finally {
+    await close()
+  }
+})
+
+test('ask_user_questions returns the cancellation message when elicitation is declined', async () => {
+  const { client, close } = await createConnectedClient({
+    onElicit: async () => ({
+      action: 'decline',
+    }),
+  })
+
+  try {
+    const result = await client.callTool({
+      name: 'ask_user_questions',
+      arguments: {
+        questions: [
+          {
+            id: 'continue',
+            header: 'Continue',
+            question: 'Continue?',
+            options: [
+              { label: 'Yes', description: 'Proceed.' },
+              { label: 'No', description: 'Stop here.' },
+            ],
+          },
+        ],
+      },
+    })
+
+    const text = result.content.find(item => item.type === 'text')
+    assert.ok(text && 'text' in text)
+    assert.equal(text.text, 'ask_user_questions was cancelled before receiving a response')
+  } finally {
+    await close()
+  }
+})
+
+test('helper formatting stays aligned with the tool contract', () => {
+  const questions = [
+    {
+      id: 'focus_areas',
+      header: 'Focus',
+      question: 'Which areas matter most?',
+      allowMultiple: true,
+      options: [
+        { label: 'Frontend', description: 'Prioritize the UI.' },
+        { label: 'Backend', description: 'Prioritize server logic.' },
+      ],
+    },
+  ]
+
+  const request = buildAskUserQuestionsElicitRequest(questions)
+  assert.equal(request.mode, 'form')
+  assert.ok(request.requestedSchema.properties.focus_areas)
+  assert.ok(!request.requestedSchema.properties['focus_areas__note'])
+
+  const formatted = formatAskUserQuestionsElicitResult(questions, {
+    action: 'accept',
+    content: {
+      focus_areas: ['Frontend', 'Backend'],
+    },
+  })
+
+  assert.equal(
+    formatted,
+    JSON.stringify({
+      answers: {
+        focus_areas: {
+          answers: ['Frontend', 'Backend'],
+        },
+      },
+    }),
+  )
+})