@goke/mcp 0.0.9 → 0.0.11

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.
package/README.md CHANGED
@@ -51,6 +51,7 @@ await addMcpCommands({
51
51
  })
52
52
 
53
53
  cli.help()
54
+ cli.completions()
54
55
  cli.parse()
55
56
  ```
56
57
 
@@ -93,6 +94,7 @@ cli.command("mcp", "Start MCP server over stdio")
93
94
  .action(createMcpAction({ cli }))
94
95
 
95
96
  cli.help()
97
+ cli.completions()
96
98
  cli.parse()
97
99
  ```
98
100
 
@@ -172,6 +174,217 @@ mcp.tool("custom-tool", "A tool defined directly", async () => ({ ... }))
172
174
  addCliToolsToMcp({ cli, server: mcp })
173
175
  ```
174
176
 
177
+ ## Multi-tenant remote MCP over HTTP
178
+
179
+ When you expose a cli as a **remote** MCP (over `StreamableHTTPServerTransport`, SSE, or any other network transport), one server process handles many concurrent users. Each tool call must run against that user's own filesystem, working directory, environment, and stdin — otherwise tenants see each other's state and stdio writes trample the JSON-RPC channel.
180
+
181
+ The recipe is:
182
+
183
+ 1. Define the cli **once**.
184
+ 2. On every new MCP session, **clone** the cli with per-tenant `{ cwd, env, fs, stdin }` and mount it on a fresh session-scoped `Server` via `addCliToolsToMcp({ cli: tenantClone, server })`.
185
+ 3. Inside command actions, always use the **injected** `ctx` — `ctx.fs`, `ctx.process.cwd`, `ctx.process.env`, `ctx.console.log` — instead of the Node globals. `@goke/mcp` wires each tool call into the tenant's cloned context, but only code that goes through `ctx` participates in that isolation.
186
+
187
+ ### Write commands against `ctx`
188
+
189
+ ```ts
190
+ import { goke } from "goke"
191
+ import { z } from "zod"
192
+ import path from "node:path"
193
+
194
+ const cli = goke("notes-app")
195
+
196
+ cli
197
+ .command("save <filename>", "Save content into the user's workspace")
198
+ .option("--content <content>", z.string().describe("File content"))
199
+ .action(async (filename, options, ctx) => {
200
+ const full = path.posix.join(ctx.process.cwd, filename)
201
+ await ctx.fs.writeFile(full, options.content)
202
+ return { saved: full, tenant: ctx.process.env.TENANT_ID }
203
+ })
204
+
205
+ cli
206
+ .command("load <filename>", "Load a file from the user's workspace")
207
+ .action(async (filename, _options, ctx) => {
208
+ const full = path.posix.join(ctx.process.cwd, filename)
209
+ const text = await ctx.fs.readFile(full, "utf8")
210
+ return { path: full, text }
211
+ })
212
+ ```
213
+
214
+ `ctx.fs` satisfies the `GokeFs` interface — a Node-compatible async filesystem API. You can point it at a real directory, a virtual in-memory store, an S3 bucket adapter, a `memfs`, or anything else you can wrap behind that interface.
215
+
216
+ ### Clone the cli per session
217
+
218
+ The MCP SDK ships `WebStandardStreamableHTTPServerTransport`, which accepts a Web-Standard `Request` and returns a `Response`. That one shape plugs directly into **any** web framework that speaks web-standard: [Spiceflow](https://github.com/remorses/spiceflow), Cloudflare Workers, Deno, Bun, Next.js route handlers, SvelteKit endpoints, or a raw `fetch`-based handler. No Express, no `node:http` wiring, no framework lock-in.
219
+
220
+ You build one `handleMcpRequest(request: Request): Promise<Response>` function and mount it wherever you route HTTP:
221
+
222
+ ```ts
223
+ import { randomUUID } from "node:crypto"
224
+ import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js"
225
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
226
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"
227
+ import { addCliToolsToMcp } from "@goke/mcp"
228
+ import type { GokeFs } from "goke"
229
+
230
+ // Wherever you store per-user state — DB, Redis, config files, etc.
231
+ declare function resolveTenant(tenantId: string): {
232
+ cwd: string
233
+ env: Record<string, string>
234
+ fs: GokeFs // your filesystem adapter
235
+ }
236
+
237
+ const transports = new Map<string, WebStandardStreamableHTTPServerTransport>()
238
+
239
+ export async function handleMcpRequest(request: Request): Promise<Response> {
240
+ // Pre-parse the body so we can use it for both routing decisions
241
+ // (is this an `initialize` request?) and as the pre-parsed body
242
+ // forwarded to the transport via `HandleRequestOptions.parsedBody`.
243
+ let parsedBody: unknown
244
+ if (request.method === "POST") {
245
+ parsedBody = await request.clone().json().catch(() => undefined)
246
+ }
247
+
248
+ const sessionId = request.headers.get("mcp-session-id")
249
+
250
+ // Existing session — route to its transport.
251
+ if (sessionId && transports.has(sessionId)) {
252
+ return transports.get(sessionId)!.handleRequest(request, { parsedBody })
253
+ }
254
+
255
+ // No session yet — must be an `initialize` request.
256
+ if (!isInitializeRequest(parsedBody)) {
257
+ return Response.json(
258
+ {
259
+ jsonrpc: "2.0",
260
+ error: { code: -32000, message: "Bad Request: No valid session ID provided" },
261
+ id: null,
262
+ },
263
+ { status: 400 },
264
+ )
265
+ }
266
+
267
+ // Derive the tenant from whatever header/cookie/JWT you use.
268
+ const tenantId = request.headers.get("x-tenant-id")
269
+ if (!tenantId) return new Response("missing x-tenant-id", { status: 401 })
270
+ const tenant = resolveTenant(tenantId)
271
+
272
+ // Clone the base cli with tenant-specific cwd/env/fs. Every tool
273
+ // call on this session now sees the tenant's state via `ctx`.
274
+ const tenantCli = baseCli.clone({
275
+ cwd: tenant.cwd,
276
+ env: { ...tenant.env, TENANT_ID: tenantId },
277
+ fs: tenant.fs,
278
+ })
279
+
280
+ const mcpServer = new McpServer(
281
+ { name: "notes-app-mcp", version: "1.0.0" },
282
+ { capabilities: {} },
283
+ )
284
+ addCliToolsToMcp({ cli: tenantCli, server: mcpServer })
285
+
286
+ const transport = new WebStandardStreamableHTTPServerTransport({
287
+ sessionIdGenerator: () => randomUUID(),
288
+ enableJsonResponse: true, // pure request/response; no SSE to manage
289
+ onsessioninitialized: (sid) => {
290
+ transports.set(sid, transport)
291
+ },
292
+ onsessionclosed: (sid) => {
293
+ transports.delete(sid)
294
+ },
295
+ })
296
+ transport.onclose = () => {
297
+ const sid = transport.sessionId
298
+ if (sid) transports.delete(sid)
299
+ }
300
+
301
+ await mcpServer.connect(transport)
302
+ return transport.handleRequest(request, { parsedBody })
303
+ }
304
+ ```
305
+
306
+ Now plug `handleMcpRequest` into whichever web runtime you use:
307
+
308
+ ```ts
309
+ // Spiceflow — runs on Node, Bun, and Cloudflare Workers with the same code
310
+ import { Spiceflow } from "spiceflow"
311
+
312
+ export const app = new Spiceflow()
313
+ .route({
314
+ method: "*",
315
+ path: "/mcp",
316
+ handler: ({ request }) => handleMcpRequest(request),
317
+ })
318
+
319
+ app.listen(3000)
320
+
321
+ // Cloudflare Workers / Deno / Bun
322
+ export default {
323
+ async fetch(request: Request): Promise<Response> {
324
+ const url = new URL(request.url)
325
+ if (url.pathname === "/mcp") return handleMcpRequest(request)
326
+ return new Response("not found", { status: 404 })
327
+ },
328
+ }
329
+
330
+ // Next.js app router
331
+ export async function POST(request: Request) {
332
+ return handleMcpRequest(request)
333
+ }
334
+ ```
335
+
336
+ **Key guarantees**
337
+
338
+ - Every tool call inside a session runs against `tenantCli`'s `cwd` / `env` / `fs` — not the base cli's and not another tenant's.
339
+ - `ctx.console.log` / `ctx.console.error` / `ctx.process.stdout.write` / `ctx.process.stderr.write` are captured into the `CallToolResult.content`. They never reach the host process stdio, so they can't corrupt the JSON-RPC channel or leak between users.
340
+ - `ctx.process.exit(code)` throws `GokeProcessExit` instead of killing the server. The tool call resolves as `{ isError: code !== 0, content: [captured output] }` and the next request keeps running.
341
+ - Actions that `throw` are caught and returned as `{ isError: true, content: [message, stderr] }`.
342
+
343
+ **Bypass hazards**
344
+
345
+ Only code that flows through `ctx` participates in the isolation. The following **bypass** it and will leak across tenants or kill the host process:
346
+
347
+ - `import fs from "node:fs"` inside a command action — reads/writes the real disk.
348
+ - `console.log(...)` / `console.error(...)` — writes to the real server stdio.
349
+ - `process.exit(1)` — terminates the entire host process.
350
+ - `process.cwd()` / `process.env.X` at module load time — snapshots the server's values, not the tenant's.
351
+
352
+ Port those to `ctx.fs`, `ctx.console`, `ctx.process.*` and you're multi-tenant-safe.
353
+
354
+ For a runnable end-to-end example (including two concurrent in-memory-fs tenants writing separate files), see [`src/__test__/http-multi-tenant.test.ts`](./src/__test__/http-multi-tenant.test.ts).
355
+
356
+ ### Lower-level primitive: `cli.createExecutionContext(override)`
357
+
358
+ If you're building a custom transport or a non-HTTP multi-tenant adapter, the underlying goke primitive is:
359
+
360
+ ```ts
361
+ import { GokeProcessExit, type GokeExecutionContextOverride } from "goke"
362
+
363
+ const override: GokeExecutionContextOverride = {
364
+ cwd: tenant.cwd,
365
+ env: tenant.env,
366
+ fs: tenant.fs,
367
+ stdin: tenant.stdin,
368
+ stdout: captureStdoutStream,
369
+ stderr: captureStderrStream,
370
+ exit: () => {}, // throw-only: the wrapper still throws GokeProcessExit
371
+ }
372
+
373
+ const ctx = cli.createExecutionContext(override)
374
+
375
+ try {
376
+ await action(...positionalArgs, options, ctx)
377
+ } catch (err) {
378
+ if (err instanceof GokeProcessExit) {
379
+ // handle the exit code — the host process is untouched
380
+ } else {
381
+ throw err
382
+ }
383
+ }
384
+ ```
385
+
386
+ `addCliToolsToMcp` builds this context for you on every tool call. Call it yourself if you need finer control (e.g. per-request capture streams for custom routing, synthetic `argv`, or serving MCP from a non-Node runtime that implements `GokeFs` differently).
387
+
175
388
  ## Full example (with config persistence)
176
389
 
177
390
  This is the pattern used by [notion-mcp-cli](../notion-mcp-cli):
@@ -241,6 +454,7 @@ cli.command('logout', 'Clear tokens').action(() => {
241
454
  })
242
455
 
243
456
  cli.help()
457
+ cli.completions()
244
458
  cli.parse()
245
459
  ```
246
460
 
@@ -20,6 +20,8 @@ function createCli() {
20
20
  const message = `Hello ${options.name}!`;
21
21
  return options.caps ? message.toUpperCase() : message;
22
22
  });
23
+ // sum-values uses wrapJsonSchema whose output is `unknown`, so values are
24
+ // cast with Number() inside the action.
23
25
  cli
24
26
  .command("sum-values", "Add two numbers")
25
27
  .option("--left <left>", wrapJsonSchema({
@@ -31,7 +33,7 @@ function createCli() {
31
33
  description: "Right operand",
32
34
  }))
33
35
  .action((options) => ({
34
- sum: options.left + options.right,
36
+ sum: Number(options.left) + Number(options.right),
35
37
  }));
36
38
  cli
37
39
  .command("echo <message>", "Echo positional message")
@@ -41,7 +43,7 @@ function createCli() {
41
43
  description: "Repeat count",
42
44
  }))
43
45
  .action((message, options) => {
44
- return message.repeat(options.repeat);
46
+ return message.repeat(Number(options.repeat));
45
47
  });
46
48
  cli
47
49
  .command("string-options", "Infer option types from plain string descriptions")
@@ -397,3 +399,222 @@ describe("addCliToolsToMcp", () => {
397
399
  }
398
400
  });
399
401
  });
402
+ /**
403
+ * Spin up a live MCP client/server pair wired to a single cli.
404
+ *
405
+ * Used by the execution-context tests below to keep the boilerplate
406
+ * out of each test body.
407
+ */
408
+ async function withMcpClient(cli, fn) {
409
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
410
+ const server = new Server({ name: "test-server", version: "1.0.0" }, { capabilities: {} });
411
+ addCliToolsToMcp({ cli, server });
412
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
413
+ try {
414
+ await server.connect(serverTransport);
415
+ await client.connect(clientTransport);
416
+ return await fn(client);
417
+ }
418
+ finally {
419
+ await client.close();
420
+ await server.close();
421
+ }
422
+ }
423
+ function textBlocks(result) {
424
+ const content = "content" in result ? result.content : [];
425
+ return content.filter((entry) => entry.type === "text").map((entry) => entry.text ?? "");
426
+ }
427
+ describe("addCliToolsToMcp execution context", () => {
428
+ it("passes an execution context as the third argument to the action", async () => {
429
+ const cli = goke("ctx-cli", {
430
+ cwd: "/workspace",
431
+ env: { TOKEN: "abc", USER: "tommy" },
432
+ stdin: "hello from stdin",
433
+ });
434
+ cli.command("inspect-ctx", "Return the injected execution context").action((_options, ctx) => {
435
+ return {
436
+ hasCtx: ctx != null,
437
+ hasConsole: typeof ctx?.console?.log === "function",
438
+ hasFs: typeof ctx?.fs?.readFile === "function",
439
+ cwd: ctx?.process?.cwd,
440
+ token: ctx?.process?.env?.TOKEN,
441
+ user: ctx?.process?.env?.USER,
442
+ stdin: ctx?.process?.stdin,
443
+ };
444
+ });
445
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "inspect-ctx", arguments: {} }));
446
+ expect(firstTextContent(result)).toMatchInlineSnapshot(`
447
+ "{
448
+ "hasCtx": true,
449
+ "hasConsole": true,
450
+ "hasFs": true,
451
+ "cwd": "/workspace",
452
+ "token": "abc",
453
+ "user": "tommy",
454
+ "stdin": "hello from stdin"
455
+ }"
456
+ `);
457
+ });
458
+ it("captures ctx.console.log output into the tool result content", async () => {
459
+ const cli = goke("logs-cli");
460
+ cli.command("noisy", "Write to ctx.console and return nothing").action((_options, ctx) => {
461
+ ctx.console.log("line one");
462
+ ctx.console.log("line", "two");
463
+ });
464
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "noisy", arguments: {} }));
465
+ expect(textBlocks(result)).toMatchInlineSnapshot(`
466
+ [
467
+ "line one
468
+ line two
469
+ ",
470
+ ]
471
+ `);
472
+ });
473
+ it("captures ctx.console.log output and still uses the action's return value", async () => {
474
+ const cli = goke("logs-plus-return-cli");
475
+ cli.command("both", "Log and return").action((_options, ctx) => {
476
+ ctx.console.log("before");
477
+ return "the-return-value";
478
+ });
479
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "both", arguments: {} }));
480
+ // Captured stdout first, then the stringified return value, as
481
+ // separate content blocks. Authors who want a single block can
482
+ // return a `{ content }` object to bypass this merging.
483
+ expect(textBlocks(result)).toMatchInlineSnapshot(`
484
+ [
485
+ "before
486
+ ",
487
+ "the-return-value",
488
+ ]
489
+ `);
490
+ });
491
+ it("treats ctx.process.exit(0) as a success result with captured content", async () => {
492
+ const cli = goke("exit-ok-cli");
493
+ cli.command("exit-ok", "Exit cleanly").action((_options, ctx) => {
494
+ ctx.console.log("all good");
495
+ ctx.process.exit(0);
496
+ });
497
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "exit-ok", arguments: {} }));
498
+ expect(result.isError).toBeFalsy();
499
+ expect(textBlocks(result)).toMatchInlineSnapshot(`
500
+ [
501
+ "all good
502
+ ",
503
+ ]
504
+ `);
505
+ });
506
+ it("treats ctx.process.exit(1) as an isError result with captured stderr", async () => {
507
+ const cli = goke("exit-fail-cli");
508
+ cli.command("exit-fail", "Exit with error").action((_options, ctx) => {
509
+ ctx.console.error("boom");
510
+ ctx.process.exit(1);
511
+ });
512
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "exit-fail", arguments: {} }));
513
+ expect(result.isError).toBe(true);
514
+ expect(textBlocks(result)).toMatchInlineSnapshot(`
515
+ [
516
+ "boom
517
+ ",
518
+ ]
519
+ `);
520
+ });
521
+ it("does not corrupt the MCP transport when the action writes to ctx.process.stdout directly", async () => {
522
+ const cli = goke("stdout-cli");
523
+ cli.command("write-stdout", "Write through ctx.process.stdout").action((_options, ctx) => {
524
+ ctx.process.stdout.write("from-process-stdout\n");
525
+ });
526
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "write-stdout", arguments: {} }));
527
+ expect(firstTextContent(result)).toBe("from-process-stdout\n");
528
+ });
529
+ it("keeps the server alive after a tool action calls ctx.process.exit", async () => {
530
+ const cli = goke("survive-cli");
531
+ cli.command("boom", "Exit with non-zero code").action((_options, ctx) => {
532
+ ctx.process.exit(2);
533
+ });
534
+ cli.command("ping", "Return a value").action(() => "pong");
535
+ await withMcpClient(cli, async (client) => {
536
+ const boomResult = await client.callTool({ name: "boom", arguments: {} });
537
+ expect(boomResult.isError).toBe(true);
538
+ // Server must still be able to serve subsequent tool calls.
539
+ const pingResult = await client.callTool({ name: "ping", arguments: {} });
540
+ expect(firstTextContent(pingResult)).toBe("pong");
541
+ });
542
+ });
543
+ it("does not include captured content when the action returns a ready-made CallToolResult", async () => {
544
+ const cli = goke("raw-cli");
545
+ cli.command("raw", "Return a raw CallToolResult").action((_options, ctx) => {
546
+ // This write should be ignored — returning a {content} object is
547
+ // the explicit escape hatch for authors who want full control.
548
+ ctx.console.log("ignored-capture");
549
+ return {
550
+ content: [
551
+ { type: "text", text: "authoritative" },
552
+ ],
553
+ };
554
+ });
555
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "raw", arguments: {} }));
556
+ expect(textBlocks(result)).toEqual(["authoritative"]);
557
+ });
558
+ it("captures ctx.console.error output on the success path", async () => {
559
+ const cli = goke("success-stderr-cli");
560
+ cli.command("warn-and-return", "Emit a warning and return a value").action((_options, ctx) => {
561
+ ctx.console.error("something suspicious");
562
+ return { ok: true };
563
+ });
564
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "warn-and-return", arguments: {} }));
565
+ // Captured stderr lands in its own text block so authors can spot
566
+ // the warning even though the action returned successfully. The
567
+ // stringified return value is appended after it.
568
+ expect(result.isError).toBeFalsy();
569
+ expect(textBlocks(result)).toMatchInlineSnapshot(`
570
+ [
571
+ "something suspicious
572
+ ",
573
+ "{
574
+ "ok": true
575
+ }",
576
+ ]
577
+ `);
578
+ });
579
+ it("captures ctx.process.stderr.write output on the success path", async () => {
580
+ const cli = goke("success-stderr-write-cli");
581
+ cli.command("warn-only", "Write to stderr and return undefined").action((_options, ctx) => {
582
+ ctx.process.stderr.write("low-level-warning\n");
583
+ });
584
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "warn-only", arguments: {} }));
585
+ expect(result.isError).toBeFalsy();
586
+ expect(textBlocks(result)).toEqual(["low-level-warning\n"]);
587
+ });
588
+ it("does not leak tool output into the cli's configured stdout/stderr", async () => {
589
+ const sentinelStdout = [];
590
+ const sentinelStderr = [];
591
+ const cli = goke("sentinel-cli", {
592
+ stdout: { write: (data) => { sentinelStdout.push(data); } },
593
+ stderr: { write: (data) => { sentinelStderr.push(data); } },
594
+ });
595
+ cli.command("noisy", "Write to both streams").action((_options, ctx) => {
596
+ ctx.console.log("stdout-chatter");
597
+ ctx.console.error("stderr-chatter");
598
+ ctx.process.stdout.write("direct-stdout\n");
599
+ ctx.process.stderr.write("direct-stderr\n");
600
+ return "value";
601
+ });
602
+ const result = await withMcpClient(cli, (client) => client.callTool({ name: "noisy", arguments: {} }));
603
+ // Everything lands in the CallToolResult — the cli's configured
604
+ // host streams must not receive a single byte during a tool call.
605
+ expect(sentinelStdout.join("")).toBe("");
606
+ expect(sentinelStderr.join("")).toBe("");
607
+ expect(textBlocks(result).join("|")).toBe("stdout-chatter\ndirect-stdout\n|stderr-chatter\ndirect-stderr\n|value");
608
+ });
609
+ it("invokes command actions with the owning cli as `this`", async () => {
610
+ const cli = goke("this-binding-cli");
611
+ let seenThis;
612
+ cli.command("whoami", "Report this-binding").action(function (_options, _ctx) {
613
+ seenThis = this;
614
+ return "ok";
615
+ });
616
+ await withMcpClient(cli, (client) => client.callTool({ name: "whoami", arguments: {} }));
617
+ // Same binding Goke#runMatchedCommand uses for parse-path actions.
618
+ expect(seenThis).toBe(cli);
619
+ });
620
+ });
@@ -148,13 +148,15 @@ describe("createMcpAction", () => {
148
148
  .action(() => {
149
149
  throw new Error("something went wrong");
150
150
  });
151
- // Wrapped JSON schema command
151
+ // Wrapped JSON schema command.
152
+ // wrapJsonSchema produces a StandardJSONSchemaV1 with `unknown` output, so
153
+ // values are cast explicitly inside the action.
152
154
  cli
153
155
  .command("config set", "Set a config value")
154
156
  .option("--key <key>", wrapJsonSchema({ type: "string", description: "Config key" }))
155
157
  .option("--value <value>", wrapJsonSchema({ type: "string", description: "Config value" }))
156
158
  .action((options) => {
157
- return `set ${options.key} = ${options.value}`;
159
+ return `set ${String(options.key)} = ${String(options.value)}`;
158
160
  });
159
161
  // Commands without actions (should NOT appear as tools)
160
162
  cli.command("no-action", "This has no action handler");
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Multi-tenant remote-MCP test.
3
+ *
4
+ * Proves that one goke cli exposed over the MCP streamable-HTTP
5
+ * transport can serve multiple concurrent users with fully isolated
6
+ * state (in-memory fs + cwd + env) — no shared host process stdio,
7
+ * no cross-tenant leaks.
8
+ *
9
+ * Wiring choices worth calling out:
10
+ *
11
+ * - `WebStandardStreamableHTTPServerTransport` from the MCP SDK
12
+ * accepts a Web-Standard `Request` and returns a `Response`.
13
+ * That means we can drive it **in-process** through the client
14
+ * transport's `fetch` hook without ever binding a TCP socket
15
+ * or spinning up `node:http` / Express. Same wire protocol,
16
+ * zero sockets.
17
+ * - `enableJsonResponse: true` switches the transport off SSE and
18
+ * into pure request/response JSON. GET SSE opens are answered
19
+ * with `405`, which the client treats as "server does not offer
20
+ * SSE" and moves on (see `_startOrAuthSse` in the SDK client).
21
+ * - Each session gets its own cli **clone** via
22
+ * `baseCli.clone({ cwd, env, fs })`. The clone inherits the
23
+ * command tree but owns its own `{ cwd, env, fs }`, which is
24
+ * what `runCliTool` forwards into every action through
25
+ * `ctx.process.*` / `ctx.fs`.
26
+ */
27
+ export {};
28
+ //# sourceMappingURL=http-multi-tenant.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-multi-tenant.test.d.ts","sourceRoot":"","sources":["../../src/__test__/http-multi-tenant.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG"}