From 595e314024e1dc60a96c3d2826046354a2c97a37 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 3 Oct 2026 19:09:45 +0200 Subject: [PATCH 1/2] fix(cli): keep the injector alive while the MCP server is running The mcp command handler resolved as soon as the server was connected, so CliService destroyed the injector and every later tool call ran on an empty container: list-templates returned no template and get-template / generate-file failed with E_TEMPLATE_UNKNOWN. Keep the handler pending until stdin closes (stdio) or for the lifetime of the process (streamable HTTP). Co-Authored-By: Claude Opus 5.5 --- packages/cli/src/commands/mcp/McpCommand.ts | 5 +++ .../mcp/utils/waitForShutdown.spec.ts | 39 +++++++++++++++++++ .../src/commands/mcp/utils/waitForShutdown.ts | 17 ++++++++ 3 files changed, 61 insertions(+) create mode 100644 packages/cli/src/commands/mcp/utils/waitForShutdown.spec.ts create mode 100644 packages/cli/src/commands/mcp/utils/waitForShutdown.ts diff --git a/packages/cli/src/commands/mcp/McpCommand.ts b/packages/cli/src/commands/mcp/McpCommand.ts index 1dab960dd..e0fb427de 100644 --- a/packages/cli/src/commands/mcp/McpCommand.ts +++ b/packages/cli/src/commands/mcp/McpCommand.ts @@ -2,6 +2,8 @@ import {command} from "@tsed/cli-core"; import {mcpServerConnect} from "@tsed/platform-mcp/cli"; import {s} from "@tsed/schema"; +import {waitForShutdown} from "./utils/waitForShutdown.js"; + const McpSchema = s.object({ http: s.boolean().default(false).description("Run MCP using HTTP server").opt("--http") }); @@ -14,5 +16,8 @@ export const McpCommand = command({ const mode = data.http ? "streamable-http" : "stdio"; await mcpServerConnect(mode); + + // the injector is destroyed when this handler resolves: stay pending while the server runs + await waitForShutdown(mode); } }).token(); diff --git a/packages/cli/src/commands/mcp/utils/waitForShutdown.spec.ts b/packages/cli/src/commands/mcp/utils/waitForShutdown.spec.ts new file mode 100644 index 000000000..57737ba6e --- /dev/null +++ b/packages/cli/src/commands/mcp/utils/waitForShutdown.spec.ts @@ -0,0 +1,39 @@ +import {EventEmitter} from "node:events"; + +import {describe, expect, it, vi} from "vitest"; + +import {waitForShutdown} from "./waitForShutdown.js"; + +async function isPending(promise: Promise) { + const pending = Symbol("pending"); + + return (await Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(pending), 10))])) === pending; +} + +describe("waitForShutdown", () => { + it("should stay pending while stdin is open (stdio)", async () => { + const stdin = new EventEmitter() as unknown as NodeJS.ReadableStream; + + expect(await isPending(waitForShutdown("stdio", stdin))).toBe(true); + }); + + it.each(["end", "close"])("should resolve when stdin emits %s (stdio)", async (event) => { + const stdin = new EventEmitter(); + const done = vi.fn(); + const promise = waitForShutdown("stdio", stdin as unknown as NodeJS.ReadableStream).then(done); + + stdin.emit(event); + await promise; + + expect(done).toHaveBeenCalledTimes(1); + }); + + it("should never resolve with the HTTP transport", async () => { + const stdin = new EventEmitter(); + const promise = waitForShutdown("streamable-http", stdin as unknown as NodeJS.ReadableStream); + + stdin.emit("end"); + + expect(await isPending(promise)).toBe(true); + }); +}); diff --git a/packages/cli/src/commands/mcp/utils/waitForShutdown.ts b/packages/cli/src/commands/mcp/utils/waitForShutdown.ts new file mode 100644 index 000000000..50346832d --- /dev/null +++ b/packages/cli/src/commands/mcp/utils/waitForShutdown.ts @@ -0,0 +1,17 @@ +/** + * Keep the `mcp` command pending while the MCP server is running. + * + * The CLI destroys the injector as soon as a command handler resolves. An MCP server keeps answering requests after + * the connection is established, so the handler must stay pending or every later tool call runs on an empty injector. + * + * - `stdio`: resolves when the client closes stdin. + * - `streamable-http`: never resolves, the HTTP server lives until the process is stopped. + */ +export function waitForShutdown(mode: "stdio" | "streamable-http", stdin: NodeJS.ReadableStream = process.stdin): Promise { + return new Promise((resolve) => { + if (mode === "stdio") { + stdin.once("end", resolve); + stdin.once("close", resolve); + } + }); +} From ca3f6e9baeaed609d3f11c029d65cf6a050d100b Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 3 Oct 2026 19:22:59 +0200 Subject: [PATCH 2/2] feat(cli): expose the architecture preference in the MCP project information set-workspace and tsed://project/info returned the convention but not the architecture, although both drive where generated files are written. Co-Authored-By: Claude Opus 5.5 --- packages/cli/src/commands/mcp/resources/projectInfoResource.ts | 1 + .../cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts | 2 ++ packages/cli/src/commands/mcp/tools/setWorkspaceTool.spec.ts | 2 ++ packages/cli/src/commands/mcp/tools/setWorkspaceTool.ts | 1 + 4 files changed, 6 insertions(+) diff --git a/packages/cli/src/commands/mcp/resources/projectInfoResource.ts b/packages/cli/src/commands/mcp/resources/projectInfoResource.ts index a47b4b8dd..7bd79f720 100644 --- a/packages/cli/src/commands/mcp/resources/projectInfoResource.ts +++ b/packages/cli/src/commands/mcp/resources/projectInfoResource.ts @@ -17,6 +17,7 @@ export const projectInfoResource = defineResource({ isInitialized: !!projectPackage.preferences?.packageManager, preferences: { convention: projectPackage.preferences.convention, + architecture: projectPackage.preferences.architecture, packageManager: projectPackage.preferences.packageManager, platform: projectPackage.preferences.platform, runtime: projectPackage.preferences.runtime diff --git a/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts b/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts index 047f9e12b..00e2b98ed 100644 --- a/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts +++ b/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts @@ -1,12 +1,14 @@ import {PackageManager} from "@tsed/cli-core"; import {s} from "@tsed/schema"; +import {ArchitectureConvention} from "../../../interfaces/ArchitectureConvention.js"; import {PlatformType} from "../../../interfaces/PlatformType.js"; import {ProjectConvention} from "../../../interfaces/ProjectConvention.js"; export const ProjectPreferenceSchema = s .object({ convention: s.string().enum(ProjectConvention).description("Project convention (Ts.ED or Angular style)"), + architecture: s.string().enum(ArchitectureConvention).description("Directory architecture (by type or by feature)"), packageManager: s.string().enum(PackageManager).description("Used project manager to install dependencies"), runtime: s .string() diff --git a/packages/cli/src/commands/mcp/tools/setWorkspaceTool.spec.ts b/packages/cli/src/commands/mcp/tools/setWorkspaceTool.spec.ts index 0c5e8f30d..61b91548f 100644 --- a/packages/cli/src/commands/mcp/tools/setWorkspaceTool.spec.ts +++ b/packages/cli/src/commands/mcp/tools/setWorkspaceTool.spec.ts @@ -18,6 +18,7 @@ describe("setWorkspaceTool", () => { cwd: "/project", preferences: { convention: "conv_default", + architecture: "feature", packageManager: "yarn", platform: "express", runtime: "node" @@ -49,6 +50,7 @@ describe("setWorkspaceTool", () => { pkg: {name: "tsed-app"}, preferences: { convention: "conv_default", + architecture: "feature", packageManager: "yarn", platform: "express", runtime: "node" diff --git a/packages/cli/src/commands/mcp/tools/setWorkspaceTool.ts b/packages/cli/src/commands/mcp/tools/setWorkspaceTool.ts index a3bbfc614..645ae343a 100644 --- a/packages/cli/src/commands/mcp/tools/setWorkspaceTool.ts +++ b/packages/cli/src/commands/mcp/tools/setWorkspaceTool.ts @@ -50,6 +50,7 @@ export const setWorkspaceTool = defineTool({ pkg: projectPackage.toJSON(), preferences: { convention: projectPackage.preferences.convention, + architecture: projectPackage.preferences.architecture, packageManager: projectPackage.preferences.packageManager, platform: projectPackage.preferences.platform, runtime: projectPackage.preferences.runtime