@goke/mcp 0.0.13 → 0.1.1

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
@@ -25,7 +25,7 @@ MCP server Your CLI
25
25
 
26
26
  1. **Discover** — calls `tools/list` on the MCP server to get every tool + its JSON Schema
27
27
  2. **Register** — creates a CLI command per tool with `--options` derived from the schema
28
- 3. **Cache** — tools and session ID are cached for 1 hour (no network on subsequent runs). Expired cache is still used when a live fetch is impossible (no token, 401 on `--help`)
28
+ 3. **Cache** — tool schemas are cached for 1 hour (no network on subsequent runs). Expired cache is still used when a live fetch is impossible (no token, 401 on `--help`)
29
29
  4. **Execute** — on invocation, connects to the server and calls the tool with coerced arguments
30
30
  5. **OAuth** — if the server returns 401, automatically opens the browser for OAuth, then retries. `--help`, `--version`, `completions`, and no-args never start OAuth
31
31
 
@@ -129,6 +129,8 @@ my-cli mcp
129
129
 
130
130
  When running as MCP, the server exposes `search` and `deploy` as tools. The `mcp` command itself is excluded. Options with Zod schemas (or any Standard Schema) become typed `inputSchema` properties in the MCP tool definition.
131
131
 
132
+ **`inputSchema.required` is not the CLI `<value>` syntax.** `--query <query>` means the flag needs a value if it is present. The flag is still optional unless the schema rejects omit (`z.string()`, not `z.string().optional()`). Required **positionals** like `<env>` always go in `required`. Schema-required flags go in `required` too. Untyped flags and `wrapJsonSchema()` flags stay optional.
133
+
132
134
  ### Installing the MCP server in clients
133
135
 
134
136
  Users can install your CLI as an MCP server in any client using [`@playwriter/install-mcp`](https://github.com/nicepkg/install-mcp) — a cross-platform tool that handles config file locations for every major MCP client:
@@ -194,12 +196,14 @@ addCliToolsToMcp({ cli, server: mcp })
194
196
 
195
197
  ## Multi-tenant remote MCP over HTTP
196
198
 
197
- 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.
199
+ When you expose a cli as a **remote** MCP over HTTP, one process handles many concurrent users. Each request 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.
200
+
201
+ Do **not** keep MCP session IDs. Streamable HTTP can run **stateless**: `sessionIdGenerator: undefined`. Each POST builds a fresh `Server` and transport, then closes them. That works on Cloudflare Workers and any other isolate that does not keep process memory.
198
202
 
199
203
  The recipe is:
200
204
 
201
205
  1. Define the cli **once**.
202
- 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 })`.
206
+ 2. On **every POST**, resolve the tenant from the request (JWT, cookie, header), **clone** the cli with `{ cwd, env, fs }`, and mount it on a fresh `Server` via `addCliToolsToMcp({ cli: tenantClone, server })`.
203
207
  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.
204
208
 
205
209
  ### Write commands against `ctx`
@@ -231,64 +235,68 @@ cli
231
235
 
232
236
  `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.
233
237
 
234
- ### Clone the cli per session
238
+ ### Clone the cli per request
235
239
 
236
240
  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.
237
241
 
242
+ Pass `sessionIdGenerator: undefined` so the transport does not emit or expect `mcp-session-id`. Do not store transports in a `Map`. JSON-RPC `initialize`, `tools/list`, and `tools/call` are separate POSTs. Each one clones the cli from the request's tenant identity.
243
+
244
+ This is MCP SDK v1 Streamable HTTP **without optional transport sessions** (protocol revisions through `2025-11-25`). It is not the later `2026-07-28` protocol, which drops `initialize`. Session IDs are optional in `2025-11-25`. Stateless servers omit them.
245
+
246
+ The Streamable HTTP spec requires **Origin** checks on every request (DNS rebinding). Reject a present, disallowed `Origin` with **403**. Missing `Origin` is normal for non-browser MCP clients.
247
+
248
+ ```
249
+ POST /mcp (JWT / x-tenant-id)
250
+
251
+ v
252
+ resolveTenant(id) ──> baseCli.clone({ cwd, env, fs })
253
+
254
+ v
255
+ fresh Server + transport
256
+ sessionIdGenerator: undefined
257
+
258
+ v
259
+ addCliToolsToMcp ──> handleRequest
260
+
261
+ v
262
+ Response
263
+
264
+ v
265
+ close transport + server ──> nothing stored
266
+ ```
267
+
238
268
  You build one `handleMcpRequest(request: Request): Promise<Response>` function and mount it wherever you route HTTP:
239
269
 
240
270
  ```ts
241
- import { randomUUID } from "node:crypto"
242
271
  import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js"
243
272
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
244
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"
245
273
  import { addCliToolsToMcp } from "@goke/mcp"
246
274
  import type { GokeFs } from "goke"
247
275
 
248
- // Wherever you store per-user state — DB, Redis, config files, etc.
249
276
  declare function resolveTenant(tenantId: string): {
250
277
  cwd: string
251
278
  env: Record<string, string>
252
- fs: GokeFs // your filesystem adapter
279
+ fs: GokeFs
253
280
  }
254
281
 
255
- const transports = new Map<string, WebStandardStreamableHTTPServerTransport>()
282
+ declare function isAllowedOrigin(origin: string): boolean
256
283
 
257
284
  export async function handleMcpRequest(request: Request): Promise<Response> {
258
- // Pre-parse the body so we can use it for both routing decisions
259
- // (is this an `initialize` request?) and as the pre-parsed body
260
- // forwarded to the transport via `HandleRequestOptions.parsedBody`.
261
- let parsedBody: unknown
262
- if (request.method === "POST") {
263
- parsedBody = await request.clone().json().catch(() => undefined)
285
+ const origin = request.headers.get("origin")
286
+ if (origin && !isAllowedOrigin(origin)) {
287
+ return new Response(null, { status: 403 })
264
288
  }
265
289
 
266
- const sessionId = request.headers.get("mcp-session-id")
267
-
268
- // Existing session — route to its transport.
269
- if (sessionId && transports.has(sessionId)) {
270
- return transports.get(sessionId)!.handleRequest(request, { parsedBody })
290
+ if (request.method !== "POST") {
291
+ return new Response(null, { status: 405, headers: { Allow: "POST" } })
271
292
  }
272
293
 
273
- // No session yet must be an `initialize` request.
274
- if (!isInitializeRequest(parsedBody)) {
275
- return Response.json(
276
- {
277
- jsonrpc: "2.0",
278
- error: { code: -32000, message: "Bad Request: No valid session ID provided" },
279
- id: null,
280
- },
281
- { status: 400 },
282
- )
283
- }
294
+ const parsedBody = await request.clone().json().catch(() => undefined)
284
295
 
285
- // Derive the tenant from whatever header/cookie/JWT you use.
286
296
  const tenantId = request.headers.get("x-tenant-id")
287
297
  if (!tenantId) return new Response("missing x-tenant-id", { status: 401 })
288
298
  const tenant = resolveTenant(tenantId)
289
299
 
290
- // Clone the base cli with tenant-specific cwd/env/fs. Every tool
291
- // call on this session now sees the tenant's state via `ctx`.
292
300
  const tenantCli = baseCli.clone({
293
301
  cwd: tenant.cwd,
294
302
  env: { ...tenant.env, TENANT_ID: tenantId },
@@ -302,22 +310,16 @@ export async function handleMcpRequest(request: Request): Promise<Response> {
302
310
  addCliToolsToMcp({ cli: tenantCli, server: mcpServer })
303
311
 
304
312
  const transport = new WebStandardStreamableHTTPServerTransport({
305
- sessionIdGenerator: () => randomUUID(),
306
- enableJsonResponse: true, // pure request/response; no SSE to manage
307
- onsessioninitialized: (sid) => {
308
- transports.set(sid, transport)
309
- },
310
- onsessionclosed: (sid) => {
311
- transports.delete(sid)
312
- },
313
+ sessionIdGenerator: undefined,
314
+ enableJsonResponse: true,
313
315
  })
314
- transport.onclose = () => {
315
- const sid = transport.sessionId
316
- if (sid) transports.delete(sid)
317
- }
318
-
319
316
  await mcpServer.connect(transport)
320
- return transport.handleRequest(request, { parsedBody })
317
+ try {
318
+ return await transport.handleRequest(request, { parsedBody })
319
+ } finally {
320
+ await transport.close()
321
+ await mcpServer.close()
322
+ }
321
323
  }
322
324
  ```
323
325
 
@@ -353,7 +355,7 @@ export async function POST(request: Request) {
353
355
 
354
356
  **Key guarantees**
355
357
 
356
- - Every tool call inside a session runs against `tenantCli`'s `cwd` / `env` / `fs` — not the base cli's and not another tenant's.
358
+ - Every request runs against `tenantCli`'s `cwd` / `env` / `fs` — not the base cli's and not another tenant's.
357
359
  - `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.
358
360
  - `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.
359
361
  - Actions that `throw` are caught and returned as `{ isError: true, content: [message, stderr] }`.
@@ -369,7 +371,7 @@ Only code that flows through `ctx` participates in the isolation. The following
369
371
 
370
372
  Port those to `ctx.fs`, `ctx.console`, `ctx.process.*` and you're multi-tenant-safe.
371
373
 
372
- 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).
374
+ For a runnable end-to-end example (including two concurrent in-memory-fs tenants writing separate files, with a new server per POST), see [`src/__test__/http-multi-tenant.test.ts`](./src/__test__/http-multi-tenant.test.ts).
373
375
 
374
376
  ### Lower-level primitive: `cli.createExecutionContext(override)`
375
377
 
@@ -486,7 +488,7 @@ Registers MCP tool commands on a goke CLI instance.
486
488
  |--------|------|---------|-------------|
487
489
  | `cli` | `Goke` | **required** | The goke CLI instance to add commands to |
488
490
  | `getMcpUrl` | `() => string \| undefined` | — | Returns the MCP server URL. Return the URL even when the user is not logged in so `--help` still works |
489
- | `getMcpTransport` | `(sessionId?) => Transport \| null` | — | Custom transport. Use for stdio or anything `getMcpUrl` cannot express |
491
+ | `getMcpTransport` | `() => Transport \| null` | — | Custom transport. Use for stdio or anything `getMcpUrl` cannot express |
490
492
  | `getHeaders` | `() => Record<string, string> \| undefined` | — | Extra HTTP headers (for example `Authorization`). Used with `getMcpUrl` |
491
493
  | `argv` | `string[]` | `process.argv.slice(2)` | Args used to skip live discovery on help and already registered commands |
492
494
  | `commandPrefix` | `string` | `''` | Prefix for commands (e.g. `'mcp'` makes `mcp notion-search`) |
@@ -554,7 +556,7 @@ Tokens are persisted via the `oauth.save()` callback you provide, so subsequent
554
556
 
555
557
  ## Caching
556
558
 
557
- Tools and the MCP session ID are cached for **1 hour** to avoid connecting on every invocation. The cache is managed through the `loadCache`/`saveCache` callbacks you control where it's stored (file, database, env, etc.).
559
+ Tool schemas are cached for **1 hour** so `--help` and command registration do not hit the network on every invocation. The cache is managed through the `loadCache`/`saveCache` callbacks. You control where it is stored (file, database, env, etc.). Each tool call still opens a new MCP connection. There is no `Mcp-Session-Id` reuse.
558
560
 
559
561
  When the cache expires or a tool call fails, the cache is cleared and tools are re-fetched on the next run.
560
562
 
@@ -178,11 +178,7 @@ describe("addCliToolsToMcp", () => {
178
178
  "type": "number",
179
179
  "description": "Right operand"
180
180
  }
181
- },
182
- "required": [
183
- "left",
184
- "right"
185
- ]
181
+ }
186
182
  }
187
183
  },
188
184
  {
@@ -224,10 +220,7 @@ describe("addCliToolsToMcp", () => {
224
220
  "type": "boolean",
225
221
  "description": "Dry run flag"
226
222
  }
227
- },
228
- "required": [
229
- "title"
230
- ]
223
+ }
231
224
  }
232
225
  }
233
226
  ]"
@@ -280,11 +273,7 @@ describe("addCliToolsToMcp", () => {
280
273
  "type": "number",
281
274
  "description": "Right operand"
282
275
  }
283
- },
284
- "required": [
285
- "left",
286
- "right"
287
- ]
276
+ }
288
277
  }
289
278
  },
290
279
  {
@@ -326,10 +315,7 @@ describe("addCliToolsToMcp", () => {
326
315
  "type": "boolean",
327
316
  "description": "Dry run flag"
328
317
  }
329
- },
330
- "required": [
331
- "title"
332
- ]
318
+ }
333
319
  }
334
320
  }
335
321
  ]"
@@ -1,7 +1,10 @@
1
1
  // First-run help and stale-cache behavior for addMcpCommands.
2
2
  import http from 'node:http';
3
3
  import { describe, expect, it } from 'vitest';
4
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
5
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
6
  import { goke } from 'goke';
7
+ import { addCliToolsToMcp } from '../cli-to-mcp.js';
5
8
  import { addMcpCommands } from '../index.js';
6
9
  const staleCache = (over = {}) => ({
7
10
  tools: [
@@ -12,7 +15,6 @@ const staleCache = (over = {}) => ({
12
15
  },
13
16
  ],
14
17
  timestamp: Date.now() - 2 * 60 * 60 * 1000,
15
- sessionId: 'stale-session',
16
18
  ...over,
17
19
  });
18
20
  function captureErrors() {
@@ -124,4 +126,64 @@ describe('addMcpCommands first-run help', () => {
124
126
  });
125
127
  expect(transportCalls).toBe(0);
126
128
  });
129
+ it('does not pass a session id into getMcpTransport', async () => {
130
+ const sessionIds = [];
131
+ const cli = goke('testcli');
132
+ await addMcpCommands({
133
+ cli,
134
+ argv: ['find_bookmarks'],
135
+ getMcpTransport: (sessionId) => {
136
+ sessionIds.push(sessionId);
137
+ return null;
138
+ },
139
+ loadCache: () => ({
140
+ tools: [
141
+ {
142
+ name: 'find_bookmarks',
143
+ description: 'Find bookmarks',
144
+ inputSchema: { type: 'object', properties: {} },
145
+ },
146
+ ],
147
+ timestamp: Date.now(),
148
+ sessionId: 'should-not-be-reused',
149
+ }),
150
+ saveCache: () => { },
151
+ });
152
+ const command = cli.commands.find((entry) => entry.name === 'find_bookmarks');
153
+ try {
154
+ await command?.commandAction?.({});
155
+ }
156
+ catch {
157
+ // transport is null, so the action exits
158
+ }
159
+ expect(sessionIds).toEqual([undefined]);
160
+ });
161
+ it('caches tools without a session id', async () => {
162
+ const saved = [];
163
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
164
+ const serverCli = goke('server-cli');
165
+ serverCli.command('ping', 'Ping').action(() => 'pong');
166
+ const server = new Server({ name: 'test-server', version: '1.0.0' }, { capabilities: {} });
167
+ addCliToolsToMcp({ cli: serverCli, server });
168
+ await server.connect(serverTransport);
169
+ const cli = goke('testcli');
170
+ try {
171
+ await addMcpCommands({
172
+ cli,
173
+ argv: ['ping'],
174
+ getMcpTransport: () => clientTransport,
175
+ loadCache: () => undefined,
176
+ saveCache: (cache) => {
177
+ if (cache)
178
+ saved.push(cache);
179
+ },
180
+ });
181
+ }
182
+ finally {
183
+ await server.close();
184
+ }
185
+ expect(saved).toHaveLength(1);
186
+ expect(saved[0]?.tools.map((tool) => tool.name)).toEqual(['ping']);
187
+ expect(saved[0]).not.toHaveProperty('sessionId');
188
+ });
127
189
  });
@@ -229,6 +229,120 @@ describe("createMcpAction", () => {
229
229
  await client.close();
230
230
  }
231
231
  });
232
+ it("keeps --flag <value> in properties but not in required", async () => {
233
+ const cli = goke("strada");
234
+ cli
235
+ .command("projects create <slug>", "Create a project")
236
+ .option("--traces-days <days>", z.string().optional().describe("Traces retention days"))
237
+ .option("--logs-days <days>", z.string().optional().describe("Logs retention days"))
238
+ .option("--errors-days <days>", z.string().optional().describe("Errors retention days"))
239
+ .option("--metrics-days <days>", z.string().optional().describe("Metrics retention days"))
240
+ .option("--all-days <days>", z.string().optional().describe("Retention for all signals"))
241
+ .action((slug, options) => ({ slug, ...options }));
242
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
243
+ cli.command("mcp", "Start MCP server").action(createMcpAction({
244
+ cli,
245
+ createTransport: () => serverTransport,
246
+ }));
247
+ cli.matchedCommandName = "mcp";
248
+ const mcpCommand = cli.commands.find((c) => c.name === "mcp");
249
+ await mcpCommand.commandAction({});
250
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
251
+ await client.connect(clientTransport);
252
+ try {
253
+ const tools = await client.listTools();
254
+ const createTool = tools.tools.find((t) => t.name === "projects_create");
255
+ expect(createTool.inputSchema.properties).toHaveProperty("slug");
256
+ expect(createTool.inputSchema.properties).toHaveProperty("tracesDays");
257
+ expect(createTool.inputSchema.properties).toHaveProperty("logsDays");
258
+ expect(createTool.inputSchema.properties).toHaveProperty("allDays");
259
+ expect(createTool.inputSchema.required).toEqual(["slug"]);
260
+ const result = await client.callTool({
261
+ name: "projects_create",
262
+ arguments: { slug: "my-app" },
263
+ });
264
+ expect(firstTextContent(result)).toBe('{\n "slug": "my-app"\n}');
265
+ await expect(client.callTool({
266
+ name: "projects_create",
267
+ arguments: {},
268
+ })).rejects.toThrow("Missing required argument: slug");
269
+ }
270
+ finally {
271
+ await client.close();
272
+ }
273
+ });
274
+ it("puts schema-required --flag <value> in inputSchema.required", async () => {
275
+ const cli = goke("checks");
276
+ cli
277
+ .command("checks create", "Create a check")
278
+ .option("--url <url>", z.string().describe("URL to check"))
279
+ .option("--name <name>", z.string().describe("Check name"))
280
+ .option("--timeout [ms]", z.number().optional().describe("Timeout"))
281
+ .action((options) => options);
282
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
283
+ cli.command("mcp", "Start MCP server").action(createMcpAction({
284
+ cli,
285
+ createTransport: () => serverTransport,
286
+ }));
287
+ cli.matchedCommandName = "mcp";
288
+ const mcpCommand = cli.commands.find((c) => c.name === "mcp");
289
+ await mcpCommand.commandAction({});
290
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
291
+ await client.connect(clientTransport);
292
+ try {
293
+ const tools = await client.listTools();
294
+ const createTool = tools.tools.find((t) => t.name === "checks_create");
295
+ expect(createTool.inputSchema.properties).toHaveProperty("url");
296
+ expect(createTool.inputSchema.properties).toHaveProperty("name");
297
+ expect(createTool.inputSchema.properties).toHaveProperty("timeout");
298
+ expect(createTool.inputSchema.required).toEqual(["url", "name"]);
299
+ const result = await client.callTool({
300
+ name: "checks_create",
301
+ arguments: { url: "https://example.com", name: "home" },
302
+ });
303
+ expect(firstTextContent(result)).toContain("https://example.com");
304
+ await expect(client.callTool({
305
+ name: "checks_create",
306
+ arguments: { name: "home" },
307
+ })).rejects.toThrow("Missing required argument: url");
308
+ }
309
+ finally {
310
+ await client.close();
311
+ }
312
+ });
313
+ it("does not put wrapJsonSchema flags in required", async () => {
314
+ const cli = goke("wrapped");
315
+ cli
316
+ .command("config set", "Set a config value")
317
+ .option("--key <key>", wrapJsonSchema({ type: "string", description: "Config key" }))
318
+ .option("--value <value>", wrapJsonSchema({ type: "string", description: "Config value" }))
319
+ .action((options) => options);
320
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
321
+ cli.command("mcp", "Start MCP server").action(createMcpAction({
322
+ cli,
323
+ createTransport: () => serverTransport,
324
+ }));
325
+ cli.matchedCommandName = "mcp";
326
+ const mcpCommand = cli.commands.find((c) => c.name === "mcp");
327
+ await mcpCommand.commandAction({});
328
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
329
+ await client.connect(clientTransport);
330
+ try {
331
+ const tools = await client.listTools();
332
+ const setTool = tools.tools.find((t) => t.name === "config_set");
333
+ expect(setTool.inputSchema.properties).toHaveProperty("key");
334
+ expect(setTool.inputSchema.properties).toHaveProperty("value");
335
+ expect(setTool.inputSchema.required).toBeUndefined();
336
+ const result = await client.callTool({
337
+ name: "config_set",
338
+ arguments: {},
339
+ });
340
+ expect(firstTextContent(result)).toBe("{}");
341
+ }
342
+ finally {
343
+ await client.close();
344
+ }
345
+ });
232
346
  it("returns empty tool list when only the mcp command exists", async () => {
233
347
  const cli = goke("empty-app");
234
348
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Multi-tenant remote-MCP test.
3
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.
4
+ * Proves that one goke cli exposed over stateless MCP streamable HTTP
5
+ * can serve multiple concurrent users with fully isolated state
6
+ * (in-memory fs + cwd + env) — no shared host process stdio, no
7
+ * cross-tenant leaks, no mcp-session-id map. Isolation comes from
8
+ * the `x-tenant-id` header on each POST, not from `Mcp-Session-Id`.
8
9
  *
9
10
  * Wiring choices worth calling out:
10
11
  *
@@ -14,11 +15,15 @@
14
15
  * transport's `fetch` hook without ever binding a TCP socket
15
16
  * or spinning up `node:http` / Express. Same wire protocol,
16
17
  * 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
18
+ * - `sessionIdGenerator: undefined` is stateless mode. The
19
+ * transport does not emit or expect `mcp-session-id`. Each
20
+ * POST builds a fresh Server + transport, then closes them.
21
+ * - `enableJsonResponse: true` switches the transport off SSE
22
+ * and into pure request/response JSON. GET SSE opens are
23
+ * answered with `405`, which the client treats as "server
24
+ * does not offer SSE" and moves on (see `_startOrAuthSse`
25
+ * in the SDK client).
26
+ * - Each request gets its own cli **clone** via
22
27
  * `baseCli.clone({ cwd, env, fs })`. The clone inherits the
23
28
  * command tree but owns its own `{ cwd, env, fs }`, which is
24
29
  * what `runCliTool` forwards into every action through
@@ -1 +1 @@
1
- {"version":3,"file":"http-multi-tenant.test.d.ts","sourceRoot":"","sources":["../../src/__test__/http-multi-tenant.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG"}
1
+ {"version":3,"file":"http-multi-tenant.test.d.ts","sourceRoot":"","sources":["../../src/__test__/http-multi-tenant.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG"}