@llm4ts/shell 2.44.1 → 2.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/Cli.d.ts +3 -7
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +20 -4
  4. package/dist/Cli.js.map +1 -1
  5. package/dist/FlowLaunch.d.ts +2 -16
  6. package/dist/FlowLaunch.d.ts.map +1 -1
  7. package/dist/FlowLaunch.js +20 -66
  8. package/dist/FlowLaunch.js.map +1 -1
  9. package/dist/FlowProcess.d.ts +67 -0
  10. package/dist/FlowProcess.d.ts.map +1 -0
  11. package/dist/FlowProcess.js +175 -0
  12. package/dist/FlowProcess.js.map +1 -0
  13. package/dist/Menu.d.ts +1 -1
  14. package/dist/Menu.d.ts.map +1 -1
  15. package/dist/OperatorActTools.d.ts +170 -0
  16. package/dist/OperatorActTools.d.ts.map +1 -0
  17. package/dist/OperatorActTools.js +332 -0
  18. package/dist/OperatorActTools.js.map +1 -0
  19. package/dist/OperatorConfig.d.ts +44 -0
  20. package/dist/OperatorConfig.d.ts.map +1 -0
  21. package/dist/OperatorConfig.js +88 -0
  22. package/dist/OperatorConfig.js.map +1 -0
  23. package/dist/OperatorNode.d.ts +45 -0
  24. package/dist/OperatorNode.d.ts.map +1 -0
  25. package/dist/OperatorNode.js +103 -0
  26. package/dist/OperatorNode.js.map +1 -0
  27. package/dist/OperatorRuns.d.ts +76 -0
  28. package/dist/OperatorRuns.d.ts.map +1 -0
  29. package/dist/OperatorRuns.js +146 -0
  30. package/dist/OperatorRuns.js.map +1 -0
  31. package/dist/OperatorServer.d.ts +160 -0
  32. package/dist/OperatorServer.d.ts.map +1 -0
  33. package/dist/OperatorServer.js +123 -0
  34. package/dist/OperatorServer.js.map +1 -0
  35. package/dist/OperatorTools.d.ts +558 -0
  36. package/dist/OperatorTools.d.ts.map +1 -0
  37. package/dist/OperatorTools.js +398 -0
  38. package/dist/OperatorTools.js.map +1 -0
  39. package/dist/Refine.d.ts +2 -2
  40. package/dist/Refine.d.ts.map +1 -1
  41. package/dist/Refine.js +3 -5
  42. package/dist/Refine.js.map +1 -1
  43. package/dist/RunSupervisor.d.ts +51 -0
  44. package/dist/RunSupervisor.d.ts.map +1 -0
  45. package/dist/RunSupervisor.js +282 -0
  46. package/dist/RunSupervisor.js.map +1 -0
  47. package/dist/ShellErrors.d.ts +9 -0
  48. package/dist/ShellErrors.d.ts.map +1 -0
  49. package/dist/ShellErrors.js +7 -0
  50. package/dist/ShellErrors.js.map +1 -0
  51. package/flows/lib/epic-stories.js +83 -30
  52. package/flows/port-guide.js +4 -1
  53. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/pack.md +1 -0
  54. package/kits/j2ee-nextjs/packs/jsp-nextjs/pack.md +1 -0
  55. package/kits/port/packs/scala-ts/pack.md +1 -0
  56. package/kits/soap-ace/fixtures/demo-bank-soap/RUNBOOK.md +5 -2
  57. package/kits/soap-ace/flows/soap-epic.js +1 -2
  58. package/package.json +13 -4
  59. package/src/Cli.ts +64 -3
  60. package/src/FlowLaunch.ts +20 -66
  61. package/src/FlowProcess.ts +239 -0
  62. package/src/OperatorActTools.ts +407 -0
  63. package/src/OperatorConfig.ts +148 -0
  64. package/src/OperatorNode.ts +147 -0
  65. package/src/OperatorRuns.ts +270 -0
  66. package/src/OperatorServer.ts +192 -0
  67. package/src/OperatorTools.ts +610 -0
  68. package/src/Refine.ts +4 -7
  69. package/src/RunSupervisor.ts +403 -0
  70. package/src/ShellErrors.ts +6 -0
@@ -0,0 +1,270 @@
1
+ import { join } from "node:path"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Option from "effect/Option"
4
+ import * as Schema from "effect/Schema"
5
+ import { EpicRun, epicRunsPath } from "@llm4ts/flow/EpicRuns"
6
+ import { describeFlowError } from "@llm4ts/flow/FlowError"
7
+ import { TraceLine } from "@llm4ts/flow/FlowRecorder"
8
+ import {
9
+ OperatorRun,
10
+ ReadFailed,
11
+ UnknownEpic,
12
+ UnknownRun,
13
+ type OperatorRunState
14
+ } from "@llm4ts/flow/OperatorSchemas"
15
+ import type { PlainFileStoreShape } from "@llm4ts/flow/Persistence"
16
+ import { readTrace } from "@llm4ts/flow/Replay"
17
+ import { TraceUnreadable } from "@llm4ts/runner/RunReaders"
18
+ import type { OperatorConfig } from "./OperatorConfig.ts"
19
+ import type { RunSupervisorShape } from "./RunSupervisor.ts"
20
+
21
+ /**
22
+ * Where the operator finds a run: the supervisor's registry first, then each
23
+ * served repository's epic `runs.jsonl` files and `.llm4ts/trace-*.jsonl`
24
+ * traces. A client never names a path; epics and runs are ids found on disk.
25
+ */
26
+
27
+ /** What run lookup reads; the operator's full dependency record extends it. */
28
+ export interface RunLookupDeps {
29
+ readonly config: OperatorConfig
30
+ readonly files: PlainFileStoreShape
31
+ /** Absolute trace paths under one `.llm4ts/` directory. */
32
+ readonly listTraces: (directory: string) => ReadonlyArray<string>
33
+ /** Folder names under `.llm4ts/epics/`. */
34
+ readonly listEpics: (directory: string) => ReadonlyArray<string>
35
+ readonly supervisor: RunSupervisorShape
36
+ }
37
+
38
+ export interface Repo {
39
+ readonly id: string
40
+ readonly path: string
41
+ }
42
+
43
+ export const stateRoot = (repo: Repo): string => join(repo.path, ".llm4ts")
44
+ export const epicsDir = (repo: Repo): string => join(stateRoot(repo), "epics")
45
+ export const transcriptsDir = (repo: Repo, runId: string): string =>
46
+ join(stateRoot(repo), "transcripts", runId)
47
+
48
+ export const readFailed = (error: unknown): ReadFailed =>
49
+ ReadFailed.make({ message: describeFlowError(error) })
50
+
51
+ /** The epic's state folder, only for an id found on disk, so no client text becomes a path. */
52
+ export const epicDirOf = (
53
+ deps: RunLookupDeps,
54
+ repo: Repo,
55
+ epic: string
56
+ ): Effect.Effect<string, UnknownEpic> =>
57
+ deps.listEpics(epicsDir(repo)).includes(epic)
58
+ ? Effect.succeed(join(epicsDir(repo), epic))
59
+ : Effect.fail(UnknownEpic.make({ message: `unknown epic '${epic}' in ${repo.id}` }))
60
+
61
+ const decodeEpicRun = Schema.decodeUnknownOption(Schema.fromJsonString(EpicRun))
62
+
63
+ /**
64
+ * An epic's runs, oldest first. A missing file is no runs and a torn last line
65
+ * (a write in progress) is skipped, but any other line that does not decode
66
+ * fails `ReadFailed` naming the file.
67
+ */
68
+ export const readEpicRunsStrict = (
69
+ files: PlainFileStoreShape,
70
+ stateDir: string
71
+ ): Effect.Effect<ReadonlyArray<EpicRun>, ReadFailed> => {
72
+ const path = epicRunsPath(stateDir)
73
+ return files.read(path).pipe(
74
+ Effect.mapError(readFailed),
75
+ Effect.flatMap((contents) => {
76
+ const rows = (contents ?? "").split("\n").filter((row) => row.trim().length > 0)
77
+ const runs: Array<EpicRun> = []
78
+ for (const [index, row] of rows.entries()) {
79
+ const decoded = Option.getOrUndefined(decodeEpicRun(row))
80
+ if (decoded !== undefined) {
81
+ runs.push(decoded)
82
+ } else if (index < rows.length - 1) {
83
+ return Effect.fail(
84
+ ReadFailed.make({ message: `malformed ${path}:${index + 1}: not an epic run record` })
85
+ )
86
+ }
87
+ }
88
+ return Effect.succeed(runs)
89
+ })
90
+ )
91
+ }
92
+
93
+ export interface EpicRunEntry {
94
+ readonly epic: string
95
+ readonly dir: string
96
+ readonly run: EpicRun
97
+ }
98
+
99
+ export const epicRunsOf = (
100
+ deps: RunLookupDeps,
101
+ repo: Repo
102
+ ): Effect.Effect<ReadonlyArray<EpicRunEntry>, ReadFailed> =>
103
+ Effect.forEach(deps.listEpics(epicsDir(repo)), (epic) => {
104
+ const dir = join(epicsDir(repo), epic)
105
+ return readEpicRunsStrict(deps.files, dir).pipe(
106
+ Effect.map((runs) => runs.map((run) => ({ epic, dir, run })))
107
+ )
108
+ }).pipe(Effect.map((all) => all.flat()))
109
+
110
+ const decodeTraceLine = Schema.decodeUnknownOption(Schema.fromJsonString(TraceLine))
111
+
112
+ export interface TraceHead {
113
+ readonly runId: string
114
+ readonly startedAt: number
115
+ readonly ended: boolean
116
+ }
117
+
118
+ /** The run a trace belongs to and whether it ended, from its first and last lines. */
119
+ export const traceHead = (
120
+ files: PlainFileStoreShape,
121
+ path: string
122
+ ): Effect.Effect<TraceHead | undefined> =>
123
+ files.read(path).pipe(
124
+ Effect.catch(() => Effect.succeed(undefined)),
125
+ Effect.map((contents) => {
126
+ const rows = (contents ?? "").split(/\r?\n/u).filter((row) => row.trim().length > 0)
127
+ const first = Option.getOrUndefined(decodeTraceLine(rows[0] ?? ""))
128
+ if (first === undefined) return undefined
129
+ const last = Option.getOrUndefined(decodeTraceLine(rows[rows.length - 1] ?? ""))
130
+ return { runId: first.runId, startedAt: first.timestamp, ended: last?.kind === "RunEnded" }
131
+ })
132
+ )
133
+
134
+ export const traceState = (head: TraceHead | undefined): OperatorRunState =>
135
+ head === undefined ? "lost" : head.ended ? "exited" : "running"
136
+
137
+ export interface ResolvedRun {
138
+ readonly runId: string
139
+ readonly repo: Repo
140
+ readonly flow: string
141
+ readonly state: OperatorRunState
142
+ readonly tracePath: string
143
+ readonly supervised?: OperatorRun
144
+ }
145
+
146
+ /** A run id through the supervisor first, then each repository's epic runs and traces. */
147
+ export const resolveRun = (
148
+ deps: RunLookupDeps,
149
+ runId: string
150
+ ): Effect.Effect<ResolvedRun, UnknownRun | ReadFailed> =>
151
+ Effect.gen(function* () {
152
+ const supervised = yield* deps.supervisor.get(runId)
153
+ if (supervised !== undefined) {
154
+ const repo = deps.config.repos.find((candidate) => candidate.id === supervised.repo)
155
+ if (repo === undefined) {
156
+ return yield* UnknownRun.make({
157
+ message: `run ${runId} belongs to repo '${supervised.repo}', which this server does not serve`
158
+ })
159
+ }
160
+ if (supervised.tracePath === undefined) {
161
+ return yield* UnknownRun.make({ message: `run ${runId} has no trace yet` })
162
+ }
163
+ return {
164
+ runId,
165
+ repo,
166
+ flow: supervised.flow,
167
+ state: supervised.state,
168
+ tracePath: supervised.tracePath,
169
+ supervised
170
+ }
171
+ }
172
+ for (const repo of deps.config.repos) {
173
+ for (const entry of yield* epicRunsOf(deps, repo)) {
174
+ if (entry.run.runId === runId) {
175
+ const head = yield* traceHead(deps.files, entry.run.tracePath)
176
+ return {
177
+ runId,
178
+ repo,
179
+ flow: "?",
180
+ state: traceState(head),
181
+ tracePath: entry.run.tracePath
182
+ }
183
+ }
184
+ }
185
+ for (const path of deps.listTraces(stateRoot(repo))) {
186
+ const head = yield* traceHead(deps.files, path)
187
+ if (head?.runId === runId) {
188
+ return { runId, repo, flow: "?", state: traceState(head), tracePath: path }
189
+ }
190
+ }
191
+ }
192
+ return yield* UnknownRun.make({ message: `unknown run '${runId}'` })
193
+ })
194
+
195
+ /** The epic a run worked on: the epic whose `runs.jsonl` lists it. */
196
+ export const epicOfRun = (
197
+ deps: RunLookupDeps,
198
+ run: ResolvedRun
199
+ ): Effect.Effect<EpicRunEntry | undefined, ReadFailed> =>
200
+ Effect.map(epicRunsOf(deps, run.repo), (entries) =>
201
+ entries.find((entry) => entry.run.runId === run.runId || entry.run.tracePath === run.tracePath)
202
+ )
203
+
204
+ /** Runs found on disk (epic run lists and traces) that the supervisor does not hold. */
205
+ export const discoveredRuns = (
206
+ deps: RunLookupDeps,
207
+ repo: Repo,
208
+ known: ReadonlySet<string>
209
+ ): Effect.Effect<ReadonlyArray<OperatorRun>, ReadFailed> =>
210
+ Effect.gen(function* () {
211
+ const found = new Map<string, OperatorRun>()
212
+ const add = (runId: string, tracePath: string, startedAt: number, state: OperatorRunState) => {
213
+ if (known.has(runId) || found.has(runId)) return
214
+ found.set(
215
+ runId,
216
+ new OperatorRun({
217
+ runId,
218
+ repo: repo.id,
219
+ flow: "?",
220
+ args: [],
221
+ knobs: {},
222
+ startedAt,
223
+ tracePath,
224
+ logDir: "",
225
+ state
226
+ })
227
+ )
228
+ }
229
+ for (const entry of yield* epicRunsOf(deps, repo)) {
230
+ const head = yield* traceHead(deps.files, entry.run.tracePath)
231
+ add(entry.run.runId, entry.run.tracePath, entry.run.startedAt, traceState(head))
232
+ }
233
+ for (const path of deps.listTraces(stateRoot(repo))) {
234
+ const head = yield* traceHead(deps.files, path)
235
+ if (head !== undefined) add(head.runId, path, head.startedAt, traceState(head))
236
+ }
237
+ return [...found.values()]
238
+ })
239
+
240
+ /**
241
+ * A run's trace lines. A supervised run whose trace file does not exist yet
242
+ * (the flow has not written its first line) reads as an empty trace; any other
243
+ * missing or unreadable trace fails `TraceUnreadable`.
244
+ */
245
+ export const runTraceLines = (
246
+ files: PlainFileStoreShape,
247
+ run: ResolvedRun
248
+ ): Effect.Effect<ReadonlyArray<TraceLine>, TraceUnreadable> =>
249
+ Effect.gen(function* () {
250
+ if (yield* tracePending(files, run)) return []
251
+ return yield* readTrace(files, run.tracePath).pipe(
252
+ Effect.mapError((error) =>
253
+ TraceUnreadable.make({ path: run.tracePath, message: describeFlowError(error) })
254
+ )
255
+ )
256
+ })
257
+
258
+ /** Whether the run is supervised and its flow has not written a trace yet. */
259
+ export const tracePending = (
260
+ files: PlainFileStoreShape,
261
+ run: ResolvedRun
262
+ ): Effect.Effect<boolean, TraceUnreadable> =>
263
+ run.supervised === undefined
264
+ ? Effect.succeed(false)
265
+ : files.read(run.tracePath).pipe(
266
+ Effect.map((contents) => contents === undefined),
267
+ Effect.mapError((error) =>
268
+ TraceUnreadable.make({ path: run.tracePath, message: describeFlowError(error) })
269
+ )
270
+ )
@@ -0,0 +1,192 @@
1
+ import { timingSafeEqual } from "node:crypto"
2
+ import { createServer } from "node:http"
3
+ import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"
4
+ import * as NodeStdio from "@effect/platform-node/NodeStdio"
5
+ import type * as Cause from "effect/Cause"
6
+ import * as Clock from "effect/Clock"
7
+ import * as Effect from "effect/Effect"
8
+ import * as Layer from "effect/Layer"
9
+ import * as Logger from "effect/Logger"
10
+ import * as Option from "effect/Option"
11
+ import * as Schema from "effect/Schema"
12
+ import * as McpProtocol from "effect/ai/McpProtocol"
13
+ import * as McpServer from "effect/ai/McpServer"
14
+ import * as Toolkit from "effect/ai/Toolkit"
15
+ import * as HttpRouter from "effect/http/HttpRouter"
16
+ import type { ServeError } from "effect/http/HttpServerError"
17
+ import * as HttpServerRequest from "effect/http/HttpServerRequest"
18
+ import * as HttpServerResponse from "effect/http/HttpServerResponse"
19
+ import {
20
+ actHandlers,
21
+ actToolkit,
22
+ approveTool,
23
+ retroStartTool,
24
+ startRunTool,
25
+ stopRunTool,
26
+ waitHandlers,
27
+ waitToolkit
28
+ } from "./OperatorActTools.ts"
29
+ import type { OperatorConfig } from "./OperatorConfig.ts"
30
+ import { readHandlers, readToolkit, type OperatorDeps } from "./OperatorTools.ts"
31
+ import { packageVersion } from "./Package.ts"
32
+
33
+ /**
34
+ * The operator MCP server (`llm4ts mcp`, ADR 0041): the read, wait and allowed
35
+ * act tools served over stdio, or over HTTP at `/mcp` behind a bearer token.
36
+ * A tool that the configuration does not allow is never registered, so it is
37
+ * absent from `tools/list` rather than refused when called.
38
+ */
39
+
40
+ export const serverName = "llm4ts"
41
+ export const serverPath = "/mcp"
42
+
43
+ type HttpTransport = Extract<OperatorConfig["transport"], { readonly _tag: "http" }>
44
+ export type HttpOperatorConfig = OperatorConfig & { readonly transport: HttpTransport }
45
+
46
+ const serverInfo = {
47
+ name: serverName,
48
+ version: packageVersion,
49
+ protocols: [McpProtocol.v2025_06_18]
50
+ } as const
51
+
52
+ const runToolkit = Toolkit.make(startRunTool, stopRunTool, retroStartTool)
53
+ const approveToolkit = Toolkit.make(approveTool)
54
+
55
+ /** The act tools a configuration allows: `--allow-run` and `--allow-approve` each add their own. */
56
+ export const actToolkitFor = (config: OperatorConfig) =>
57
+ config.allowRun
58
+ ? config.allowApprove
59
+ ? actToolkit
60
+ : runToolkit
61
+ : config.allowApprove
62
+ ? approveToolkit
63
+ : Toolkit.empty
64
+
65
+ const actLayer = (config: OperatorConfig, deps: OperatorDeps): Layer.Layer<never> => {
66
+ const act = actHandlers(deps)
67
+ if (config.allowRun && config.allowApprove) {
68
+ return McpServer.toolkit(actToolkit).pipe(Layer.provide(actToolkit.toLayer(act)))
69
+ }
70
+ if (config.allowRun) {
71
+ const handlers = runToolkit.of({
72
+ start_run: act.start_run,
73
+ stop_run: act.stop_run,
74
+ retro_start: act.retro_start
75
+ })
76
+ return McpServer.toolkit(runToolkit).pipe(Layer.provide(runToolkit.toLayer(handlers)))
77
+ }
78
+ if (config.allowApprove) {
79
+ const handlers = approveToolkit.of({ approve: act.approve })
80
+ return McpServer.toolkit(approveToolkit).pipe(Layer.provide(approveToolkit.toLayer(handlers)))
81
+ }
82
+ return Layer.empty
83
+ }
84
+
85
+ /** Every tool this configuration serves, registered on the server it is provided. */
86
+ export const toolLayer = (
87
+ config: OperatorConfig,
88
+ deps: OperatorDeps
89
+ ): Layer.Layer<never, never, McpServer.McpServer> =>
90
+ Layer.mergeAll(
91
+ McpServer.toolkit(readToolkit).pipe(Layer.provide(readToolkit.toLayer(readHandlers(deps)))),
92
+ McpServer.toolkit(waitToolkit).pipe(Layer.provide(waitToolkit.toLayer(waitHandlers(deps)))),
93
+ actLayer(config, deps)
94
+ )
95
+
96
+ const unauthorized = HttpServerResponse.empty({ status: 401 })
97
+
98
+ /**
99
+ * Lets a request through only with `Authorization: Bearer <token>`, compared in
100
+ * constant time; anything else is a bare 401. The token is never logged.
101
+ */
102
+ export const bearerMiddleware = (token: string): Layer.Layer<never> => {
103
+ const expected = Buffer.from(`Bearer ${token}`, "utf8")
104
+ const matches = (header: string | undefined): boolean => {
105
+ if (header === undefined) return false
106
+ const actual = Buffer.from(header, "utf8")
107
+ return actual.length === expected.length && timingSafeEqual(actual, expected)
108
+ }
109
+ return HttpRouter.middleware((httpEffect) =>
110
+ Effect.gen(function* () {
111
+ const request = yield* HttpServerRequest.HttpServerRequest
112
+ return matches(request.headers.authorization) ? yield* httpEffect : unauthorized
113
+ })
114
+ ).layer
115
+ }
116
+
117
+ /** The HTTP application: the tools on an MCP endpoint at `/mcp`, behind the bearer token. */
118
+ export const httpAppLayer = (
119
+ config: HttpOperatorConfig,
120
+ deps: OperatorDeps
121
+ ): Layer.Layer<never, Cause.IllegalArgumentError, HttpRouter.HttpRouter> =>
122
+ toolLayer(config, deps).pipe(
123
+ Layer.provide(McpServer.layerHttp({ ...serverInfo, path: serverPath })),
124
+ Layer.provide(bearerMiddleware(config.transport.token))
125
+ )
126
+
127
+ const RpcCall = Schema.Struct({
128
+ method: Schema.String,
129
+ params: Schema.optional(Schema.Struct({ name: Schema.optional(Schema.String) }))
130
+ })
131
+ const decodeRpcCall = Schema.decodeUnknownOption(Schema.fromJsonString(RpcCall))
132
+
133
+ /** What a request asked for: its JSON-RPC method and, for `tools/call`, the tool. Never its arguments. */
134
+ const requestLabel = (body: string): string =>
135
+ Option.match(decodeRpcCall(body), {
136
+ onNone: () => "request",
137
+ onSome: (call) =>
138
+ call.method === "tools/call" && call.params?.name !== undefined
139
+ ? `tools/call ${call.params.name}`
140
+ : call.method
141
+ })
142
+
143
+ /**
144
+ * One log line per HTTP request: method, tool, duration and status. The body
145
+ * is read only after the endpoint has read it (the read is cached), and never
146
+ * for a request the token check turned away.
147
+ */
148
+ const requestLog = HttpRouter.middleware(
149
+ (httpEffect) =>
150
+ Effect.gen(function* () {
151
+ const request = yield* HttpServerRequest.HttpServerRequest
152
+ const startedAt = yield* Clock.currentTimeMillis
153
+ const response = yield* httpEffect
154
+ const elapsed = (yield* Clock.currentTimeMillis) - startedAt
155
+ const label =
156
+ response.status === 401 || request.method !== "POST"
157
+ ? request.method
158
+ : yield* request.text.pipe(
159
+ Effect.map(requestLabel),
160
+ Effect.orElseSucceed(() => "request")
161
+ )
162
+ yield* Effect.logInfo(`mcp ${label} ${elapsed}ms status=${response.status}`)
163
+ return response
164
+ }),
165
+ { global: true }
166
+ )
167
+
168
+ /** Serves the operator tools on the configured transport until interrupted. */
169
+ export const runOperatorServer = (
170
+ config: OperatorConfig,
171
+ deps: OperatorDeps
172
+ ): Effect.Effect<never, ServeError | Cause.IllegalArgumentError> => {
173
+ const transport = config.transport
174
+ if (transport._tag === "stdio") {
175
+ // stdout carries the protocol: every log line goes to stderr.
176
+ return Layer.launch(
177
+ toolLayer(config, deps).pipe(
178
+ Layer.provide(McpServer.layerStdio(serverInfo)),
179
+ Layer.provide(NodeStdio.layer)
180
+ )
181
+ ).pipe(Effect.provideService(Logger.LogToStderr, true))
182
+ }
183
+ return Layer.launch(
184
+ HttpRouter.serve(Layer.merge(httpAppLayer({ ...config, transport }, deps), requestLog), {
185
+ disableLogger: true
186
+ }).pipe(
187
+ Layer.provide(
188
+ NodeHttpServer.layer(createServer, { host: transport.host, port: transport.port })
189
+ )
190
+ )
191
+ )
192
+ }