Merge pull request #3957 from jeremymcs/fix/mcp-importLocalModule-paths

fix(mcp-server): importLocalModule resolves src/ paths from dist/ context
This commit is contained in:
Jeremy McSpadden 2026-04-10 19:54:00 -05:00 committed by GitHub
commit 9a7453fae8
2 changed files with 82 additions and 1 deletions

View file

@ -0,0 +1,48 @@
// GSD-2 — Regression tests for importLocalModule candidate resolution (#3954)
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { _buildImportCandidates } from "./workflow-tools.js";
describe("_buildImportCandidates", () => {
it("includes dist/ fallback for src/ paths", () => {
const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
assert.ok(
candidates.some((c) => c.includes("/dist/resources/extensions/gsd/db-writer.js")),
"should include dist/ swapped candidate",
);
});
it("includes src/ fallback for dist/ paths", () => {
const candidates = _buildImportCandidates("../../../dist/resources/extensions/gsd/db-writer.js");
assert.ok(
candidates.some((c) => c.includes("/src/resources/extensions/gsd/db-writer.js")),
"should include src/ swapped candidate",
);
});
it("includes .ts variants for .js paths", () => {
const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
assert.ok(
candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/src/")),
"should include .ts variant for original src/ path",
);
assert.ok(
candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/dist/")),
"should include .ts variant for swapped dist/ path",
);
});
it("returns original path first", () => {
const input = "../../../src/resources/extensions/gsd/db-writer.js";
const candidates = _buildImportCandidates(input);
assert.equal(candidates[0], input, "first candidate should be the original path");
});
it("handles paths without src/ or dist/ gracefully", () => {
const candidates = _buildImportCandidates("./local-module.js");
assert.equal(candidates.length, 2, "should have original + .ts variant only");
assert.equal(candidates[0], "./local-module.js");
assert.equal(candidates[1], "./local-module.ts");
});
});

View file

@ -326,6 +326,7 @@ function getWriteGateModuleCandidates(): string[] {
candidates.push(
new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url).href,
new URL("../../../dist/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url).href,
new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.ts", import.meta.url).href,
);
@ -336,8 +337,39 @@ function toFileUrl(modulePath: string): string {
return pathToFileURL(resolve(modulePath)).href;
}
/** @internal — exported for testing only */
export function _buildImportCandidates(relativePath: string): string[] {
// Build candidate paths: try the given path first, then swap src/<->dist/
// and try .ts extension. This handles both dev (tsx from src/) and prod
// (compiled from dist/) execution contexts.
const candidates: string[] = [relativePath];
const swapped = relativePath.includes("/src/")
? relativePath.replace("/src/", "/dist/")
: relativePath.includes("/dist/")
? relativePath.replace("/dist/", "/src/")
: null;
if (swapped) candidates.push(swapped);
// Also try .ts variants for dev-mode tsx execution
if (relativePath.endsWith(".js")) {
candidates.push(relativePath.replace(/\.js$/, ".ts"));
if (swapped) candidates.push(swapped.replace(/\.js$/, ".ts"));
}
return candidates;
}
async function importLocalModule<T>(relativePath: string): Promise<T> {
return import(new URL(relativePath, import.meta.url).href) as Promise<T>;
const candidates = _buildImportCandidates(relativePath)
.map((p) => new URL(p, import.meta.url).href);
let lastErr: unknown;
for (const candidate of candidates) {
try {
return await import(candidate) as T;
} catch (err) {
lastErr = err;
}
}
throw lastErr;
}
function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.env): string[] {
@ -352,6 +384,7 @@ function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.en
candidates.push(
new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url).href,
new URL("../../../dist/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,
);