fix(gsd): harden claude-code workflow MCP bootstrap
Ensure startup/session/init paths auto-prepare workflow MCP for Claude Code. Disable native AskUserQuestion in Claude Code SDK options to avoid broken host prompts. Add explicit /gsd mcp init . guidance when workflow MCP is missing. Refs #3964
This commit is contained in:
parent
ef91a17bb2
commit
4301a72522
9 changed files with 359 additions and 28 deletions
|
|
@ -184,6 +184,7 @@ export function makeStreamExhaustedErrorMessage(model: string, lastTextContent:
|
|||
*/
|
||||
export function buildSdkOptions(modelId: string, prompt: string): Record<string, unknown> {
|
||||
const mcpServers = buildWorkflowMcpServers();
|
||||
const disallowedTools = ["AskUserQuestion"];
|
||||
return {
|
||||
pathToClaudeCodeExecutable: getClaudePath(),
|
||||
model: modelId,
|
||||
|
|
@ -194,6 +195,7 @@ export function buildSdkOptions(modelId: string, prompt: string): Record<string,
|
|||
allowDangerouslySkipPermissions: true,
|
||||
settingSources: ["project"],
|
||||
systemPrompt: { type: "preset", preset: "claude_code" },
|
||||
disallowedTools,
|
||||
...(mcpServers ? { mcpServers } : {}),
|
||||
betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -217,6 +217,35 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|||
assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
|
||||
assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
|
||||
assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, "/tmp/project");
|
||||
assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
|
||||
} finally {
|
||||
process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
|
||||
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
||||
process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
|
||||
process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
|
||||
process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
|
||||
}
|
||||
});
|
||||
|
||||
test("buildSdkOptions disables AskUserQuestion for custom workflow MCP server names", () => {
|
||||
const prev = {
|
||||
GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
|
||||
GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
|
||||
GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
|
||||
GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
|
||||
GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
|
||||
};
|
||||
try {
|
||||
process.env.GSD_WORKFLOW_MCP_COMMAND = "node";
|
||||
process.env.GSD_WORKFLOW_MCP_NAME = "custom-workflow";
|
||||
process.env.GSD_WORKFLOW_MCP_ARGS = JSON.stringify(["packages/mcp-server/dist/cli.js"]);
|
||||
process.env.GSD_WORKFLOW_MCP_ENV = JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" });
|
||||
process.env.GSD_WORKFLOW_MCP_CWD = "/tmp/project";
|
||||
|
||||
const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
|
||||
const mcpServers = options.mcpServers as Record<string, any>;
|
||||
assert.ok(mcpServers?.["custom-workflow"], "expected custom workflow server config");
|
||||
assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
|
||||
} finally {
|
||||
process.env.GSD_WORKFLOW_MCP_COMMAND = prev.GSD_WORKFLOW_MCP_COMMAND;
|
||||
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
||||
|
|
@ -252,6 +281,9 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|||
const mcpServers = (options as any).mcpServers;
|
||||
if (mcpServers) {
|
||||
assert.ok(mcpServers["gsd-workflow"], "if present, must be gsd-workflow");
|
||||
assert.deepEqual((options as any).disallowedTools, ["AskUserQuestion"]);
|
||||
} else {
|
||||
assert.deepEqual((options as any).disallowedTools, ["AskUserQuestion"]);
|
||||
}
|
||||
rmSync(emptyDir, { recursive: true, force: true });
|
||||
} finally {
|
||||
|
|
@ -298,6 +330,7 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|||
assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
|
||||
assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
|
||||
assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, resolvedRepoDir);
|
||||
assert.deepEqual(options.disallowedTools, ["AskUserQuestion"]);
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
rmSync(repoDir, { recursive: true, force: true });
|
||||
|
|
|
|||
|
|
@ -335,19 +335,9 @@ export async function bootstrapAutoSession(
|
|||
}
|
||||
}
|
||||
|
||||
if (ctx.model?.provider === "claude-code") {
|
||||
try {
|
||||
const { ensureProjectWorkflowMcpConfig } = await import("./mcp-project-config.js");
|
||||
const result = ensureProjectWorkflowMcpConfig(base);
|
||||
if (result.status !== "unchanged") {
|
||||
ctx.ui.notify(`Claude Code MCP prepared at ${result.configPath}`, "info");
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.ui.notify(
|
||||
`Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
{
|
||||
const { prepareWorkflowMcpForProject } = await import("./workflow-mcp-auto-prep.js");
|
||||
prepareWorkflowMcpForProject(ctx, base);
|
||||
}
|
||||
|
||||
// Initialize GitServiceImpl
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|||
resetToolCallLoopGuard();
|
||||
resetAskUserQuestionsCache();
|
||||
await syncServiceTierStatus(ctx);
|
||||
const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
|
||||
prepareWorkflowMcpForProject(ctx, process.cwd());
|
||||
|
||||
// Apply show_token_cost preference (#1515)
|
||||
try {
|
||||
|
|
@ -85,6 +87,8 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|||
resetAskUserQuestionsCache();
|
||||
clearDiscussionFlowState();
|
||||
await syncServiceTierStatus(ctx);
|
||||
const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
|
||||
prepareWorkflowMcpForProject(ctx, process.cwd());
|
||||
loadToolApiKeys();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -274,19 +274,9 @@ export async function showProjectInit(
|
|||
// Non-fatal — STATE.md will be regenerated on next /gsd invocation
|
||||
}
|
||||
|
||||
if (ctx.model?.provider === "claude-code") {
|
||||
try {
|
||||
const { ensureProjectWorkflowMcpConfig } = await import("./mcp-project-config.js");
|
||||
const result = ensureProjectWorkflowMcpConfig(basePath);
|
||||
if (result.status !== "unchanged") {
|
||||
ctx.ui.notify(`Claude Code MCP prepared at ${result.configPath}`, "info");
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.ui.notify(
|
||||
`Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
{
|
||||
const { prepareWorkflowMcpForProject } = await import("./workflow-mcp-auto-prep.js");
|
||||
prepareWorkflowMcpForProject(ctx, basePath);
|
||||
}
|
||||
|
||||
ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { prepareWorkflowMcpForProject, shouldAutoPrepareWorkflowMcp } from "../workflow-mcp-auto-prep.ts";
|
||||
|
||||
test("shouldAutoPrepareWorkflowMcp enables prep for externalCli local transport", () => {
|
||||
const result = shouldAutoPrepareWorkflowMcp({
|
||||
model: { provider: "claude-code", baseUrl: "local://claude-code" },
|
||||
modelRegistry: {
|
||||
getProviderAuthMode: () => "externalCli",
|
||||
isProviderRequestReady: () => false,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, true);
|
||||
});
|
||||
|
||||
test("shouldAutoPrepareWorkflowMcp enables prep when claude-code provider is ready", () => {
|
||||
const result = shouldAutoPrepareWorkflowMcp({
|
||||
model: { provider: "openai", baseUrl: "https://api.openai.com" },
|
||||
modelRegistry: {
|
||||
getProviderAuthMode: () => "apiKey",
|
||||
isProviderRequestReady: (provider: string) => provider === "claude-code",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, true);
|
||||
});
|
||||
|
||||
test("shouldAutoPrepareWorkflowMcp enables prep when claude-code provider is registered", () => {
|
||||
const result = shouldAutoPrepareWorkflowMcp({
|
||||
model: { provider: "openai", baseUrl: "https://api.openai.com" },
|
||||
modelRegistry: {
|
||||
getProviderAuthMode: (provider: string) => provider === "claude-code" ? "externalCli" : "apiKey",
|
||||
isProviderRequestReady: () => false,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, true);
|
||||
});
|
||||
|
||||
test("shouldAutoPrepareWorkflowMcp stays disabled when neither transport nor provider readiness match", () => {
|
||||
const result = shouldAutoPrepareWorkflowMcp({
|
||||
model: { provider: "openai", baseUrl: "https://api.openai.com" },
|
||||
modelRegistry: {
|
||||
getProviderAuthMode: () => "apiKey",
|
||||
isProviderRequestReady: () => false,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, false);
|
||||
});
|
||||
|
||||
test("prepareWorkflowMcpForProject warns with /gsd mcp init guidance when prep fails", () => {
|
||||
const notifications: Array<{ message: string; level: string }> = [];
|
||||
const result = prepareWorkflowMcpForProject(
|
||||
{
|
||||
model: { provider: "claude-code", baseUrl: "local://claude-code" },
|
||||
modelRegistry: {
|
||||
getProviderAuthMode: () => "externalCli",
|
||||
isProviderRequestReady: () => true,
|
||||
},
|
||||
ui: {
|
||||
notify: (message: string, level: string) => {
|
||||
notifications.push({ message, level });
|
||||
},
|
||||
},
|
||||
},
|
||||
"/",
|
||||
);
|
||||
|
||||
assert.equal(result, null);
|
||||
assert.equal(notifications.length, 1);
|
||||
assert.equal(notifications[0].level, "warning");
|
||||
assert.match(notifications[0].message, /Please run \/gsd mcp init \./);
|
||||
});
|
||||
|
|
@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
import {
|
||||
buildWorkflowMcpServers,
|
||||
|
|
@ -184,7 +185,34 @@ test("workflow MCP launch config reaches mutation tools over stdio", async () =>
|
|||
assert.match(launch.env?.NODE_OPTIONS ?? "", /resolve-ts\.mjs/);
|
||||
}
|
||||
|
||||
const client = new Client({ name: "workflow-mcp-transport-test", version: "1.0.0" });
|
||||
const client = new Client(
|
||||
{ name: "workflow-mcp-transport-test", version: "1.0.0" },
|
||||
{ capabilities: { elicitation: {} } },
|
||||
);
|
||||
client.setRequestHandler(ElicitRequestSchema, 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.transport_mode);
|
||||
assert.ok(elicitation.requestedSchema.properties["transport_mode__note"]);
|
||||
assert.ok(elicitation.requestedSchema.required?.includes("transport_mode"));
|
||||
|
||||
return {
|
||||
action: "accept",
|
||||
content: {
|
||||
transport_mode: "None of the above",
|
||||
transport_mode__note: "Need Windows-safe MCP elicitation.",
|
||||
},
|
||||
};
|
||||
});
|
||||
const transport = new StdioClientTransport({
|
||||
command: launch.command,
|
||||
args: launch.args,
|
||||
|
|
@ -206,6 +234,38 @@ test("workflow MCP launch config reaches mutation tools over stdio", async () =>
|
|||
"expected workflow MCP surface to expose ask_user_questions",
|
||||
);
|
||||
|
||||
const askResult = await client.callTool(
|
||||
{
|
||||
name: "ask_user_questions",
|
||||
arguments: {
|
||||
questions: [
|
||||
{
|
||||
id: "transport_mode",
|
||||
header: "Transport",
|
||||
question: "How should the workflow prompt be delivered?",
|
||||
options: [
|
||||
{ label: "Local UI", description: "Use the host tool UI." },
|
||||
{ label: "Remote UI", description: "Use a remote response channel." },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
assert.equal(askResult.isError, undefined);
|
||||
assert.equal(
|
||||
((askResult.content as Array<{ text?: string }>)?.[0])?.text ?? "",
|
||||
JSON.stringify({
|
||||
answers: {
|
||||
transport_mode: {
|
||||
answers: ["None of the above", "user_note: Need Windows-safe MCP elicitation."],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const milestoneResult = await client.callTool(
|
||||
{
|
||||
name: "gsd_plan_milestone",
|
||||
|
|
@ -285,6 +345,101 @@ test("workflow MCP launch config reaches mutation tools over stdio", async () =>
|
|||
}
|
||||
});
|
||||
|
||||
test("workflow MCP ask_user_questions uses stdio elicitation round-trip", async () => {
|
||||
const projectRoot = mkdtempSync(join(tmpdir(), "gsd-workflow-elicit-"));
|
||||
mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
|
||||
|
||||
const launch = detectWorkflowMcpLaunchConfig(projectRoot, {});
|
||||
assert.ok(launch, "expected a workflow MCP launch config");
|
||||
|
||||
const client = new Client(
|
||||
{ name: "workflow-mcp-elicit-test", version: "1.0.0" },
|
||||
{ capabilities: { elicitation: {} } },
|
||||
);
|
||||
let requestSeen: {
|
||||
message: string;
|
||||
requestedSchema: { properties: Record<string, unknown>; required?: string[] };
|
||||
} | null = null;
|
||||
|
||||
client.setRequestHandler(ElicitRequestSchema, async (request) => {
|
||||
const params = (
|
||||
request as {
|
||||
params?: {
|
||||
message: string;
|
||||
requestedSchema: { properties: Record<string, unknown>; required?: string[] };
|
||||
};
|
||||
}
|
||||
).params ?? request as {
|
||||
message: string;
|
||||
requestedSchema: { properties: Record<string, unknown>; required?: string[] };
|
||||
};
|
||||
|
||||
requestSeen = params;
|
||||
|
||||
return {
|
||||
action: "accept",
|
||||
content: {
|
||||
deployment: "None of the above",
|
||||
deployment__note: "Need hybrid deployment.",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: launch.command,
|
||||
args: launch.args,
|
||||
env: { ...process.env, ...launch.env } as Record<string, string>,
|
||||
cwd: launch.cwd,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect(transport, { timeout: 30_000 });
|
||||
|
||||
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." },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
assert.ok(requestSeen, "expected stdio transport to forward an elicitation request");
|
||||
assert.match(requestSeen.message, /Please answer the following question/);
|
||||
assert.ok(requestSeen.requestedSchema.properties.deployment);
|
||||
assert.ok(requestSeen.requestedSchema.properties.deployment__note);
|
||||
assert.ok(requestSeen.requestedSchema.required?.includes("deployment"));
|
||||
|
||||
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 client.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("usesWorkflowMcpTransport matches local externalCli providers", () => {
|
||||
assert.equal(usesWorkflowMcpTransport("externalCli", "local://claude-code"), true);
|
||||
assert.equal(usesWorkflowMcpTransport("externalCli", "https://api.example.com"), false);
|
||||
|
|
@ -514,3 +669,8 @@ test("auto phases source enforces workflow compatibility preflight", () => {
|
|||
assert.match(src, /getWorkflowTransportSupportError/);
|
||||
assert.match(src, /workflow-capability/);
|
||||
});
|
||||
|
||||
test("workflow transport error guidance includes /gsd mcp init hint", () => {
|
||||
const src = readSrc("workflow-mcp.ts");
|
||||
assert.match(src, /Please run \/gsd mcp init \./);
|
||||
});
|
||||
|
|
|
|||
76
src/resources/extensions/gsd/workflow-mcp-auto-prep.ts
Normal file
76
src/resources/extensions/gsd/workflow-mcp-auto-prep.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
||||
|
||||
import {
|
||||
type EnsureProjectWorkflowMcpConfigResult,
|
||||
ensureProjectWorkflowMcpConfig,
|
||||
} from "./mcp-project-config.js";
|
||||
import { usesWorkflowMcpTransport } from "./workflow-mcp.js";
|
||||
|
||||
interface WorkflowMcpAutoPrepContext {
|
||||
model?: { provider?: string; baseUrl?: string };
|
||||
modelRegistry?: {
|
||||
getProviderAuthMode?: (provider: string) => string;
|
||||
isProviderRequestReady?: (provider: string) => boolean;
|
||||
};
|
||||
ui?: Pick<ExtensionContext["ui"], "notify">;
|
||||
}
|
||||
|
||||
function getAuthModeSafe(
|
||||
ctx: WorkflowMcpAutoPrepContext,
|
||||
provider: string | undefined,
|
||||
): string | undefined {
|
||||
if (!provider) return undefined;
|
||||
const getAuthMode = ctx.modelRegistry?.getProviderAuthMode;
|
||||
if (typeof getAuthMode !== "function") return undefined;
|
||||
try {
|
||||
return getAuthMode(provider);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function hasClaudeCodeProvider(ctx: WorkflowMcpAutoPrepContext): boolean {
|
||||
return getAuthModeSafe(ctx, "claude-code") === "externalCli";
|
||||
}
|
||||
|
||||
function isClaudeCodeProviderReady(ctx: WorkflowMcpAutoPrepContext): boolean {
|
||||
const readyCheck = ctx.modelRegistry?.isProviderRequestReady;
|
||||
if (typeof readyCheck !== "function") return false;
|
||||
try {
|
||||
return readyCheck("claude-code");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldAutoPrepareWorkflowMcp(ctx: WorkflowMcpAutoPrepContext): boolean {
|
||||
const provider = ctx.model?.provider;
|
||||
const baseUrl = ctx.model?.baseUrl;
|
||||
const authMode = getAuthModeSafe(ctx, provider);
|
||||
|
||||
if (usesWorkflowMcpTransport(authMode as any, baseUrl)) return true;
|
||||
if (provider === "claude-code") return true;
|
||||
if (hasClaudeCodeProvider(ctx)) return true;
|
||||
return isClaudeCodeProviderReady(ctx);
|
||||
}
|
||||
|
||||
export function prepareWorkflowMcpForProject(
|
||||
ctx: WorkflowMcpAutoPrepContext,
|
||||
projectRoot: string,
|
||||
): EnsureProjectWorkflowMcpConfigResult | null {
|
||||
if (!shouldAutoPrepareWorkflowMcp(ctx)) return null;
|
||||
|
||||
try {
|
||||
const result = ensureProjectWorkflowMcpConfig(projectRoot);
|
||||
if (result.status !== "unchanged") {
|
||||
ctx.ui?.notify?.(`Claude Code MCP prepared at ${result.configPath}`, "info");
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
ctx.ui?.notify?.(
|
||||
`Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}. Detected Claude Code model but no workflow MCP. Please run /gsd mcp init . from your project root.`,
|
||||
"warning",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -364,7 +364,7 @@ export function getWorkflowTransportSupportError(
|
|||
const providerLabel = `"${provider}"`;
|
||||
|
||||
if (!launch) {
|
||||
return `Provider ${providerLabel} cannot run ${surface}${unitLabel}: the GSD workflow MCP server is not configured or discoverable. Configure GSD_WORKFLOW_MCP_COMMAND, build packages/mcp-server/dist/cli.js, or install gsd-mcp-server on PATH.`;
|
||||
return `Provider ${providerLabel} cannot run ${surface}${unitLabel}: the GSD workflow MCP server is not configured or discoverable. Detected Claude Code model but no workflow MCP. Please run /gsd mcp init . from your project root. You can also configure GSD_WORKFLOW_MCP_COMMAND, build packages/mcp-server/dist/cli.js, or install gsd-mcp-server on PATH.`;
|
||||
}
|
||||
|
||||
const missing = [...new Set(requiredTools)].filter((tool) => !MCP_WORKFLOW_TOOL_SURFACE.has(tool));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue