chore: harden workflow MCP executor loading

This commit is contained in:
Jeremy 2026-04-09 12:11:59 -05:00
parent d116cff601
commit 60a5bf6ace
3 changed files with 81 additions and 7 deletions

View file

@ -4,6 +4,11 @@ MCP server exposing GSD orchestration tools for Claude Code, Cursor, and other M
Start GSD auto-mode sessions, poll progress, resolve blockers, and retrieve results — all through the [Model Context Protocol](https://modelcontextprotocol.io/).
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
## Installation
```bash
@ -69,6 +74,38 @@ Add to `.cursor/mcp.json`:
## Tools
### Workflow mutation tools
The workflow MCP surface includes:
- `gsd_plan_milestone`
- `gsd_plan_slice`
- `gsd_replan_slice`
- `gsd_slice_replan`
- `gsd_task_complete`
- `gsd_complete_task`
- `gsd_slice_complete`
- `gsd_complete_slice`
- `gsd_validate_milestone`
- `gsd_milestone_validate`
- `gsd_complete_milestone`
- `gsd_milestone_complete`
- `gsd_reassess_roadmap`
- `gsd_roadmap_reassess`
- `gsd_save_gate_result`
- `gsd_summary_save`
- `gsd_milestone_status`
These mutation tools use the same GSD workflow handlers as the native in-process tool path.
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
- 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.
### `gsd_execute`
Start a GSD auto-mode session for a project directory.
@ -175,6 +212,7 @@ Resolve a pending blocker in a session by sending a response to the blocked UI r
| Variable | Description |
|----------|-------------|
| `GSD_CLI_PATH` | Absolute path to the GSD CLI binary. If not set, the server resolves `gsd` via `which`. |
| `GSD_WORKFLOW_EXECUTORS_MODULE` | Optional absolute path or `file:` URL for the shared GSD workflow executor module used by workflow mutation tools. |
## Architecture

View file

@ -1,9 +1,9 @@
/**
* MCP Server registers GSD orchestration + read-only project state tools.
* 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
* Read-only tools (6): gsd_progress, gsd_roadmap, gsd_history, gsd_doctor, gsd_captures, gsd_knowledge
* Workflow tools (3): gsd_summary_save, gsd_task_complete, gsd_milestone_status
* Workflow tools (17): planning, replanning, completion, validation, reassessment, gate result, and milestone status tools
*
* Uses dynamic imports for @modelcontextprotocol/sdk because TS Node16
* cannot resolve the SDK's subpath exports statically (same pattern as
@ -117,7 +117,7 @@ interface McpServerInstance {
// ---------------------------------------------------------------------------
/**
* Create and configure an MCP server with 12 GSD tools (6 session + 6 read-only).
* Create and configure an MCP server with session, read-only, and workflow tools.
*
* Returns the McpServer instance call `connect(transport)` to start serving.
* Uses dynamic imports for the MCP SDK to avoid TS subpath resolution issues.

View file

@ -2,6 +2,8 @@
* Workflow MCP tools exposes the core GSD mutation/read handlers over MCP.
*/
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { z } from "zod";
const SUMMARY_ARTIFACT_TYPES = ["SUMMARY", "RESEARCH", "CONTEXT", "ASSESSMENT", "CONTEXT-DRAFT"] as const;
@ -217,12 +219,46 @@ type WorkflowToolExecutors = {
let workflowToolExecutorsPromise: Promise<WorkflowToolExecutors> | null = null;
function toFileUrl(modulePath: string): string {
return pathToFileURL(resolve(modulePath)).href;
}
function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.env): string[] {
const candidates: string[] = [];
const explicitModule = env.GSD_WORKFLOW_EXECUTORS_MODULE?.trim();
if (explicitModule) {
candidates.push(
explicitModule.startsWith("file:") || explicitModule.startsWith("data:") ? explicitModule : toFileUrl(explicitModule),
);
}
candidates.push(
new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url).href,
new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.ts", import.meta.url).href,
);
return [...new Set(candidates)];
}
async function getWorkflowToolExecutors(): Promise<WorkflowToolExecutors> {
if (!workflowToolExecutorsPromise) {
const jsUrl = new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url).href;
const tsUrl = new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.ts", import.meta.url).href;
workflowToolExecutorsPromise = import(jsUrl)
.catch(() => import(tsUrl)) as Promise<WorkflowToolExecutors>;
workflowToolExecutorsPromise = (async () => {
const attempts: string[] = [];
for (const candidate of getWorkflowExecutorModuleCandidates()) {
try {
return await import(candidate) as WorkflowToolExecutors;
} catch (err) {
attempts.push(`${candidate} (${err instanceof Error ? err.message : String(err)})`);
}
}
throw new Error(
"Unable to load GSD workflow executor bridge for MCP mutation tools. " +
"Set GSD_WORKFLOW_EXECUTORS_MODULE to an importable workflow-tool-executors module, " +
"or run the MCP server from a GSD checkout that includes src/resources/extensions/gsd/tools/workflow-tool-executors.(js|ts). " +
`Attempts: ${attempts.join("; ")}`,
);
})();
}
return workflowToolExecutorsPromise;
}