Skip to content

Commit 89bb0de

Browse files
committed
refactor(cli-mcp): dispatch mcp via single NodeRuntime.runMain with Layer.launch ternary
Replace the imperative process.argv guard + dynamic import with a top-level ternary that feeds Layer.launch(ServerLayer) or the CLI pipeline into a single NodeRuntime.runMain. Also fixes list_releases destructiveHint (false, not true).
1 parent 0c0c70c commit 89bb0de

2 files changed

Lines changed: 75 additions & 74 deletions

File tree

tools/cli/src/main.ts

Lines changed: 69 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { generateCommand } from './commands/generate.js';
1010
import { initCommand } from './commands/init.js';
1111
import { releasesCommand } from './commands/releases.js';
1212
import { updateCommand } from './commands/update.js';
13-
import { mcpCommand } from './mcp.js';
13+
import { mcpCommand, ServerLayer } from './mcp.js';
1414
import { GithubReleaseLayer } from './services/release.js';
1515

1616
function readCliVersion(): string {
@@ -28,11 +28,6 @@ function readCliVersion(): string {
2828
}
2929
}
3030

31-
if (process.argv[2] === 'mcp') {
32-
const { runMcpServer } = await import('./mcp.js');
33-
runMcpServer();
34-
}
35-
3631
const version = readCliVersion();
3732

3833
const rootCommand = Command.make('ping-lf').pipe(
@@ -53,67 +48,71 @@ const cli = Command.run(rootCommand, {
5348
version,
5449
});
5550

56-
cli(process.argv).pipe(
57-
Effect.catchTag('DirectoryConflictError', (err) =>
58-
Console.error(
59-
`\nError: "${err.path}" already contains a framework project.` +
60-
`\n Did you mean to use --local?\n` +
61-
`\n ping-lf init <new-directory> --local ${err.path}\n`,
62-
).pipe(Effect.andThen(Effect.die(err))),
63-
),
64-
Effect.catchTag('DirectoryNotEmptyError', (err) =>
65-
Console.error(
66-
`\nError: "${err.path}" already exists and is not empty.` +
67-
`\n Choose a different directory name, or delete the existing directory first.\n`,
68-
).pipe(Effect.andThen(Effect.die(err))),
69-
),
70-
Effect.catchTag('ReleaseNetworkError', (err) =>
71-
Console.error(
72-
`\nError: Could not reach GitHub to download the release.\n` +
73-
` ${err.cause}\n\n` +
74-
` • Check your network connection and try again.\n` +
75-
` • Use a local path: ping-lf init <dir> --local <path>\n` +
76-
` • Specify a version: ping-lf init <dir> --version v1.0.0\n`,
77-
).pipe(Effect.andThen(Effect.die(err))),
78-
),
79-
Effect.catchTag('ReleaseParseError', (err) =>
80-
Console.error(`\nError: Failed to parse the release data.\n ${err.cause}\n`).pipe(
81-
Effect.andThen(Effect.die(err)),
82-
),
83-
),
84-
Effect.catchTag('ReleaseFsError', (err) =>
85-
Console.error(
86-
`\nError: Filesystem error during release download (${err.operation}).\n ${err.cause}\n`,
87-
).pipe(Effect.andThen(Effect.die(err))),
88-
),
89-
Effect.catchTag('InvalidVersionError', (err) =>
90-
Console.error(
91-
`\nError: "${err.version}" is not a valid version tag.\n` +
92-
` Expected semver format like v1.0.0.\n` +
93-
` Use "ping-lf releases" to list available versions.\n`,
94-
).pipe(Effect.andThen(Effect.die(err))),
95-
),
96-
Effect.catchTag('ReleaseNotFoundError', (err) =>
97-
Console.error(
98-
`\nError: No releases found on GitHub.` +
99-
(err.cause ? `\n ${err.cause}` : '') +
100-
`\n\n • Check your network connection and try again.\n` +
101-
` • Use a local path: ping-lf init <dir> --local <path>\n`,
102-
).pipe(Effect.andThen(Effect.die(err))),
103-
),
104-
Effect.catchTag('InvalidComponentNameError', (err) =>
105-
Console.error(
106-
`\nError: "${err.name}" is not a valid component name.\n` +
107-
` Names must be PascalCase, start with an uppercase letter, and contain only letters and digits.\n` +
108-
` Examples: MyCallback, JWTLogin, DefaultStage\n`,
109-
).pipe(Effect.andThen(Effect.die(err))),
110-
),
111-
Effect.catchTag('ComponentAlreadyExistsError', (err) =>
112-
Console.error(
113-
`\nError: component directory already exists: ${err.path}\n` +
114-
` Choose a different name or delete the existing directory first.\n`,
115-
).pipe(Effect.andThen(Effect.die(err))),
116-
),
117-
Effect.provide(Layer.mergeAll(GithubReleaseLayer, NodeContext.layer)),
118-
(effect) => NodeRuntime.runMain(effect, { disableErrorReporting: true }),
119-
);
51+
const program =
52+
process.argv[2] === 'mcp'
53+
? Layer.launch(ServerLayer)
54+
: cli(process.argv).pipe(
55+
Effect.catchTag('DirectoryConflictError', (err) =>
56+
Console.error(
57+
`\nError: "${err.path}" already contains a framework project.` +
58+
`\n Did you mean to use --local?\n` +
59+
`\n ping-lf init <new-directory> --local ${err.path}\n`,
60+
).pipe(Effect.andThen(Effect.die(err))),
61+
),
62+
Effect.catchTag('DirectoryNotEmptyError', (err) =>
63+
Console.error(
64+
`\nError: "${err.path}" already exists and is not empty.` +
65+
`\n Choose a different directory name, or delete the existing directory first.\n`,
66+
).pipe(Effect.andThen(Effect.die(err))),
67+
),
68+
Effect.catchTag('ReleaseNetworkError', (err) =>
69+
Console.error(
70+
`\nError: Could not reach GitHub to download the release.\n` +
71+
` ${err.cause}\n\n` +
72+
` • Check your network connection and try again.\n` +
73+
` • Use a local path: ping-lf init <dir> --local <path>\n` +
74+
` • Specify a version: ping-lf init <dir> --version v1.0.0\n`,
75+
).pipe(Effect.andThen(Effect.die(err))),
76+
),
77+
Effect.catchTag('ReleaseParseError', (err) =>
78+
Console.error(`\nError: Failed to parse the release data.\n ${err.cause}\n`).pipe(
79+
Effect.andThen(Effect.die(err)),
80+
),
81+
),
82+
Effect.catchTag('ReleaseFsError', (err) =>
83+
Console.error(
84+
`\nError: Filesystem error during release download (${err.operation}).\n ${err.cause}\n`,
85+
).pipe(Effect.andThen(Effect.die(err))),
86+
),
87+
Effect.catchTag('InvalidVersionError', (err) =>
88+
Console.error(
89+
`\nError: "${err.version}" is not a valid version tag.\n` +
90+
` Expected semver format like v1.0.0.\n` +
91+
` Use "ping-lf releases" to list available versions.\n`,
92+
).pipe(Effect.andThen(Effect.die(err))),
93+
),
94+
Effect.catchTag('ReleaseNotFoundError', (err) =>
95+
Console.error(
96+
`\nError: No releases found on GitHub.` +
97+
(err.cause ? `\n ${err.cause}` : '') +
98+
`\n\n • Check your network connection and try again.\n` +
99+
` • Use a local path: ping-lf init <dir> --local <path>\n`,
100+
).pipe(Effect.andThen(Effect.die(err))),
101+
),
102+
Effect.catchTag('InvalidComponentNameError', (err) =>
103+
Console.error(
104+
`\nError: "${err.name}" is not a valid component name.\n` +
105+
` Names must be PascalCase, start with an uppercase letter, and contain only letters and digits.\n` +
106+
` Examples: MyCallback, JWTLogin, DefaultStage\n`,
107+
).pipe(Effect.andThen(Effect.die(err))),
108+
),
109+
Effect.catchTag('ComponentAlreadyExistsError', (err) =>
110+
Console.error(
111+
`\nError: component directory already exists: ${err.path}\n` +
112+
` Choose a different name or delete the existing directory first.\n`,
113+
).pipe(Effect.andThen(Effect.die(err))),
114+
),
115+
Effect.provide(Layer.mergeAll(GithubReleaseLayer, NodeContext.layer)),
116+
);
117+
118+
NodeRuntime.runMain(program, { disableErrorReporting: true });

tools/cli/src/mcp.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { McpServer, Tool, Toolkit } from '@effect/ai';
22
import { Command } from '@effect/cli';
3-
import { NodeContext, NodeRuntime, NodeSink, NodeStream } from '@effect/platform-node';
3+
import { NodeContext, NodeSink, NodeStream } from '@effect/platform-node';
44
import { Cause, Effect, Layer, Logger, Option, Schema } from 'effect';
55
import { existsSync, readFileSync } from 'node:fs';
66
import { dirname, resolve } from 'node:path';
@@ -165,6 +165,7 @@ const ListReleasesTool = Tool.make('list_releases', {
165165
failure: Schema.String,
166166
})
167167
.annotate(Tool.Readonly, true)
168+
.annotate(Tool.Destructive, false)
168169
.annotate(Tool.OpenWorld, true)
169170
.annotate(Tool.Idempotent, true);
170171

@@ -262,11 +263,12 @@ const ServerLayer = McpServer.toolkit(mcpToolkit).pipe(
262263
),
263264
);
264265

265-
export const runMcpServer = () => Layer.launch(ServerLayer).pipe(NodeRuntime.runMain);
266+
export { ServerLayer };
266267

267268
// mcpCommand is registered only so `ping-lf --help` lists `mcp` as a subcommand.
268-
// The actual dispatch happens in main.ts before the @effect/cli pipeline runs,
269-
// because Layer.launch is incompatible with the CLI's layer composition.
269+
// The actual MCP dispatch happens in main.ts via Layer.launch(ServerLayer) at the
270+
// top level, outside the @effect/cli command handler where Layer.launch would
271+
// nest a second fiber scope inside the CLI's already-running one.
270272
export const mcpCommand = Command.make('mcp', {}, () => Effect.void).pipe(
271273
Command.withDescription('Start as an MCP server over stdio.'),
272274
);

0 commit comments

Comments
 (0)