@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.
@@ -292,6 +292,153 @@ describe("createMcpAction", () => {
292
292
  }
293
293
  });
294
294
 
295
+ it("keeps --flag <value> in properties but not in required", async () => {
296
+ const cli = goke("strada");
297
+
298
+ cli
299
+ .command("projects create <slug>", "Create a project")
300
+ .option("--traces-days <days>", z.string().optional().describe("Traces retention days"))
301
+ .option("--logs-days <days>", z.string().optional().describe("Logs retention days"))
302
+ .option("--errors-days <days>", z.string().optional().describe("Errors retention days"))
303
+ .option("--metrics-days <days>", z.string().optional().describe("Metrics retention days"))
304
+ .option("--all-days <days>", z.string().optional().describe("Retention for all signals"))
305
+ .action((slug, options) => ({ slug, ...options }));
306
+
307
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
308
+
309
+ cli.command("mcp", "Start MCP server").action(
310
+ createMcpAction({
311
+ cli,
312
+ createTransport: () => serverTransport,
313
+ }),
314
+ );
315
+
316
+ cli.matchedCommandName = "mcp";
317
+ const mcpCommand = cli.commands.find((c) => c.name === "mcp")!;
318
+ await mcpCommand.commandAction!({});
319
+
320
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
321
+ await client.connect(clientTransport);
322
+
323
+ try {
324
+ const tools = await client.listTools();
325
+ const createTool = tools.tools.find((t) => t.name === "projects_create")!;
326
+ expect(createTool.inputSchema.properties).toHaveProperty("slug");
327
+ expect(createTool.inputSchema.properties).toHaveProperty("tracesDays");
328
+ expect(createTool.inputSchema.properties).toHaveProperty("logsDays");
329
+ expect(createTool.inputSchema.properties).toHaveProperty("allDays");
330
+ expect(createTool.inputSchema.required).toEqual(["slug"]);
331
+
332
+ const result = await client.callTool({
333
+ name: "projects_create",
334
+ arguments: { slug: "my-app" },
335
+ });
336
+ expect(firstTextContent(result)).toBe('{\n "slug": "my-app"\n}');
337
+
338
+ await expect(
339
+ client.callTool({
340
+ name: "projects_create",
341
+ arguments: {},
342
+ }),
343
+ ).rejects.toThrow("Missing required argument: slug");
344
+ } finally {
345
+ await client.close();
346
+ }
347
+ });
348
+
349
+ it("puts schema-required --flag <value> in inputSchema.required", async () => {
350
+ const cli = goke("checks");
351
+
352
+ cli
353
+ .command("checks create", "Create a check")
354
+ .option("--url <url>", z.string().describe("URL to check"))
355
+ .option("--name <name>", z.string().describe("Check name"))
356
+ .option("--timeout [ms]", z.number().optional().describe("Timeout"))
357
+ .action((options) => options);
358
+
359
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
360
+
361
+ cli.command("mcp", "Start MCP server").action(
362
+ createMcpAction({
363
+ cli,
364
+ createTransport: () => serverTransport,
365
+ }),
366
+ );
367
+
368
+ cli.matchedCommandName = "mcp";
369
+ const mcpCommand = cli.commands.find((c) => c.name === "mcp")!;
370
+ await mcpCommand.commandAction!({});
371
+
372
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
373
+ await client.connect(clientTransport);
374
+
375
+ try {
376
+ const tools = await client.listTools();
377
+ const createTool = tools.tools.find((t) => t.name === "checks_create")!;
378
+ expect(createTool.inputSchema.properties).toHaveProperty("url");
379
+ expect(createTool.inputSchema.properties).toHaveProperty("name");
380
+ expect(createTool.inputSchema.properties).toHaveProperty("timeout");
381
+ expect(createTool.inputSchema.required).toEqual(["url", "name"]);
382
+
383
+ const result = await client.callTool({
384
+ name: "checks_create",
385
+ arguments: { url: "https://example.com", name: "home" },
386
+ });
387
+ expect(firstTextContent(result)).toContain("https://example.com");
388
+
389
+ await expect(
390
+ client.callTool({
391
+ name: "checks_create",
392
+ arguments: { name: "home" },
393
+ }),
394
+ ).rejects.toThrow("Missing required argument: url");
395
+ } finally {
396
+ await client.close();
397
+ }
398
+ });
399
+
400
+ it("does not put wrapJsonSchema flags in required", async () => {
401
+ const cli = goke("wrapped");
402
+
403
+ cli
404
+ .command("config set", "Set a config value")
405
+ .option("--key <key>", wrapJsonSchema({ type: "string", description: "Config key" }))
406
+ .option("--value <value>", wrapJsonSchema({ type: "string", description: "Config value" }))
407
+ .action((options) => options);
408
+
409
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
410
+
411
+ cli.command("mcp", "Start MCP server").action(
412
+ createMcpAction({
413
+ cli,
414
+ createTransport: () => serverTransport,
415
+ }),
416
+ );
417
+
418
+ cli.matchedCommandName = "mcp";
419
+ const mcpCommand = cli.commands.find((c) => c.name === "mcp")!;
420
+ await mcpCommand.commandAction!({});
421
+
422
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
423
+ await client.connect(clientTransport);
424
+
425
+ try {
426
+ const tools = await client.listTools();
427
+ const setTool = tools.tools.find((t) => t.name === "config_set")!;
428
+ expect(setTool.inputSchema.properties).toHaveProperty("key");
429
+ expect(setTool.inputSchema.properties).toHaveProperty("value");
430
+ expect(setTool.inputSchema.required).toBeUndefined();
431
+
432
+ const result = await client.callTool({
433
+ name: "config_set",
434
+ arguments: {},
435
+ });
436
+ expect(firstTextContent(result)).toBe("{}");
437
+ } finally {
438
+ await client.close();
439
+ }
440
+ });
441
+
295
442
  it("returns empty tool list when only the mcp command exists", async () => {
296
443
  const cli = goke("empty-app");
297
444
 
@@ -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,19 +15,21 @@
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
25
30
  * `ctx.process.*` / `ctx.fs`.
26
31
  */
27
32
 
28
- import { randomUUID } from "node:crypto";
29
- import { Buffer } from "node:buffer";
30
33
  import path from "node:path";
31
34
 
32
35
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -34,7 +37,6 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
34
37
  import { Server as McpLowLevelServer } from "@modelcontextprotocol/sdk/server/index.js";
35
38
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
36
39
  import type { FetchLike } from "@modelcontextprotocol/sdk/shared/transport.js";
37
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
38
40
  import { goke, type Goke, type GokeFs } from "goke";
39
41
  import { describe, expect, it } from "vitest";
40
42
  import { z } from "zod";
@@ -94,7 +96,7 @@ class InMemoryFs implements GokeFs {
94
96
  * One cli definition, reused across tenants. Commands read / write
95
97
  * through `ctx.fs` and resolve paths against `ctx.process.cwd`, so
96
98
  * the *same* code runs per tenant but talks to a tenant-specific
97
- * filesystem when invoked via the session-scoped clone below.
99
+ * filesystem when invoked via the per-request clone below.
98
100
  */
99
101
  function buildBaseCli(): Goke {
100
102
  const cli = goke("notes-app");
@@ -122,9 +124,8 @@ function buildBaseCli(): Goke {
122
124
  // ─── In-process multi-tenant fetch ────────────────────────────────
123
125
 
124
126
  /**
125
- * Per-tenant state resolved from the `x-tenant-id` header on a
126
- * session-initialization request. Each tenant gets its own cwd,
127
- * env, and in-memory fs.
127
+ * Per-tenant state resolved from the `x-tenant-id` header on every
128
+ * POST. Each tenant gets its own cwd, env, and in-memory fs.
128
129
  */
129
130
  interface TenantState {
130
131
  cwd: string;
@@ -133,41 +134,40 @@ interface TenantState {
133
134
  }
134
135
 
135
136
  /**
136
- * Build a `FetchLike` that routes MCP streamable-HTTP traffic into
137
- * in-process session-scoped `WebStandardStreamableHTTPServerTransport`
138
- * instances. One transport + one cli clone per session. Each session
139
- * is keyed by `mcp-session-id`; initialization requests pick a tenant
140
- * via the `x-tenant-id` header.
141
- *
142
- * Returns both the custom fetch and the transports map so tests can
143
- * inspect session state if needed.
137
+ * Build a `FetchLike` that handles each MCP POST with a fresh
138
+ * `WebStandardStreamableHTTPServerTransport` and cli clone.
139
+ * Tenant identity comes from `x-tenant-id` on every request.
140
+ * Nothing is keyed by `mcp-session-id`.
144
141
  */
145
142
  function createMultiTenantFetch(options: {
146
143
  baseCli: Goke;
147
144
  resolveTenant: (tenantId: string) => TenantState;
148
145
  }): {
149
146
  fetch: FetchLike;
150
- transports: Map<string, WebStandardStreamableHTTPServerTransport>;
147
+ sessionHeaders: string[];
151
148
  } {
152
149
  const { baseCli, resolveTenant } = options;
153
- const transports = new Map<string, WebStandardStreamableHTTPServerTransport>();
150
+ const sessionHeaders: string[] = [];
154
151
 
155
152
  const customFetch: FetchLike = async (url, init) => {
156
153
  const method = (init?.method ?? "GET").toUpperCase();
157
154
  const headers = new Headers(init?.headers);
155
+ const sessionHeader = headers.get("mcp-session-id");
156
+ if (sessionHeader) {
157
+ sessionHeaders.push(sessionHeader);
158
+ }
159
+
160
+ const origin = headers.get("origin");
161
+ if (origin && origin !== "http://in-memory-mcp.test") {
162
+ return new Response(null, { status: 403 });
163
+ }
158
164
 
159
- // Pure request/response mode: tell the client there's no SSE
160
- // available on GET. `_startOrAuthSse` in the SDK client treats
161
- // 405 as "server does not offer SSE" and moves on gracefully.
162
- if (method === "GET") {
163
- return new Response(null, { status: 405 });
165
+ if (method !== "POST") {
166
+ return new Response(null, { status: 405, headers: { Allow: "POST" } });
164
167
  }
165
168
 
166
- // Parse POST body once and hand it to the transport via
167
- // `parsedBody` in `HandleRequestOptions` so we don't have to
168
- // worry about Request body streams being single-use.
169
169
  let parsedBody: unknown = undefined;
170
- if (method === "POST" && init?.body != null) {
170
+ if (init?.body != null) {
171
171
  const rawBody = init.body;
172
172
  const bodyText = typeof rawBody === "string"
173
173
  ? rawBody
@@ -177,35 +177,11 @@ function createMultiTenantFetch(options: {
177
177
  }
178
178
  }
179
179
 
180
- // Rebuild a plain Request with the same method + headers. The
181
- // transport reads accept/content-type from here and uses
182
- // `parsedBody` for the actual JSON-RPC payload.
183
180
  const request = new Request(url.toString(), {
184
181
  method,
185
182
  headers,
186
183
  });
187
184
 
188
- const sessionId = headers.get("mcp-session-id");
189
-
190
- // Existing session: route to its transport.
191
- if (sessionId && transports.has(sessionId)) {
192
- return transports.get(sessionId)!.handleRequest(request, { parsedBody });
193
- }
194
-
195
- // New session: must be an initialize POST.
196
- if (method !== "POST" || !isInitializeRequest(parsedBody)) {
197
- return new Response(
198
- JSON.stringify({
199
- jsonrpc: "2.0",
200
- error: { code: -32000, message: "Bad Request: No valid session ID provided" },
201
- id: null,
202
- }),
203
- { status: 400, headers: { "content-type": "application/json" } },
204
- );
205
- }
206
-
207
- // Resolve the tenant from the custom header, build a cli clone
208
- // with its cwd/env/fs, and spin up a session-scoped MCP server.
209
185
  const tenantId = headers.get("x-tenant-id");
210
186
  if (!tenantId) {
211
187
  return new Response("missing x-tenant-id header", { status: 401 });
@@ -225,29 +201,20 @@ function createMultiTenantFetch(options: {
225
201
  addCliToolsToMcp({ cli: tenantCli, server: mcpServer });
226
202
 
227
203
  const transport = new WebStandardStreamableHTTPServerTransport({
228
- sessionIdGenerator: () => randomUUID(),
229
- // Pure request/response — no SSE streaming to clean up.
204
+ sessionIdGenerator: undefined,
230
205
  enableJsonResponse: true,
231
- onsessioninitialized: (sid) => {
232
- transports.set(sid, transport);
233
- },
234
- onsessionclosed: (sid) => {
235
- transports.delete(sid);
236
- },
237
206
  });
238
207
 
239
- transport.onclose = () => {
240
- const sid = transport.sessionId;
241
- if (sid) {
242
- transports.delete(sid);
243
- }
244
- };
245
-
246
208
  await mcpServer.connect(transport);
247
- return transport.handleRequest(request, { parsedBody });
209
+ try {
210
+ return await transport.handleRequest(request, { parsedBody });
211
+ } finally {
212
+ await transport.close();
213
+ await mcpServer.close();
214
+ }
248
215
  };
249
216
 
250
- return { fetch: customFetch, transports };
217
+ return { fetch: customFetch, sessionHeaders };
251
218
  }
252
219
 
253
220
  // ─── Tests ────────────────────────────────────────────────────────
@@ -267,7 +234,7 @@ describe("remote MCP over streamable HTTP with multi-tenant in-memory fs", () =>
267
234
  fs: new InMemoryFs(),
268
235
  });
269
236
 
270
- const { fetch: tenantFetch } = createMultiTenantFetch({
237
+ const { fetch: tenantFetch, sessionHeaders } = createMultiTenantFetch({
271
238
  baseCli,
272
239
  resolveTenant: (id) => {
273
240
  const tenant = tenants.get(id);
@@ -276,8 +243,6 @@ describe("remote MCP over streamable HTTP with multi-tenant in-memory fs", () =>
276
243
  },
277
244
  });
278
245
 
279
- // The URL is a placeholder — the in-process fetch never looks
280
- // at the host, just the method/headers/body.
281
246
  const endpoint = new URL("http://in-memory-mcp.test/mcp");
282
247
 
283
248
  async function connectTenant(tenantId: string): Promise<Client> {
@@ -297,7 +262,7 @@ describe("remote MCP over streamable HTTP with multi-tenant in-memory fs", () =>
297
262
  return client;
298
263
  }
299
264
 
300
- return { tenants, connectTenant };
265
+ return { tenants, connectTenant, sessionHeaders };
301
266
  }
302
267
 
303
268
  function firstTextBlock(result: Awaited<ReturnType<Client["callTool"]>>): string {
@@ -305,23 +270,18 @@ describe("remote MCP over streamable HTTP with multi-tenant in-memory fs", () =>
305
270
  return content.find((block) => block.type === "text")?.text ?? "";
306
271
  }
307
272
 
308
- it("routes each session to its own cli clone with tenant-specific cwd/env/fs", async () => {
309
- const { tenants, connectTenant } = setupScenario();
273
+ it("routes each request to its own cli clone with tenant-specific cwd/env/fs", async () => {
274
+ const { tenants, connectTenant, sessionHeaders } = setupScenario();
310
275
 
311
276
  const aliceClient = await connectTenant("tenant-a");
312
277
  const bobClient = await connectTenant("tenant-b");
313
278
 
314
279
  try {
315
- // Each client sees the same tool catalog — it comes from the
316
- // shared cli definition.
317
280
  const aliceTools = (await aliceClient.listTools()).tools.map((t) => t.name).sort();
318
281
  const bobTools = (await bobClient.listTools()).tools.map((t) => t.name).sort();
319
282
  expect(aliceTools).toEqual(["load", "save"]);
320
283
  expect(bobTools).toEqual(["load", "save"]);
321
284
 
322
- // Both tenants write a file called `notes.txt` with different
323
- // content. Since each session uses its own cli clone (with
324
- // its own cwd + fs), the writes land in separate Maps.
325
285
  const aliceSave = await aliceClient.callTool({
326
286
  name: "save",
327
287
  arguments: { filename: "notes.txt", content: "alice-secret" },
@@ -336,7 +296,6 @@ describe("remote MCP over streamable HTTP with multi-tenant in-memory fs", () =>
336
296
  expect(firstTextBlock(bobSave)).toContain("/workspace-b/notes.txt");
337
297
  expect(firstTextBlock(bobSave)).toContain("tenant-b");
338
298
 
339
- // Each tenant reads back what it wrote.
340
299
  const aliceLoad = await aliceClient.callTool({
341
300
  name: "load",
342
301
  arguments: { filename: "notes.txt" },
@@ -351,14 +310,13 @@ describe("remote MCP over streamable HTTP with multi-tenant in-memory fs", () =>
351
310
  expect(firstTextBlock(bobLoad)).toContain("bob-secret");
352
311
  expect(firstTextBlock(bobLoad)).not.toContain("alice-secret");
353
312
 
354
- // Sanity check: the underlying in-memory maps really are
355
- // disjoint. Tenant A's fs only has tenant A's file.
356
313
  const tenantAFs = tenants.get("tenant-a")!.fs;
357
314
  const tenantBFs = tenants.get("tenant-b")!.fs;
358
315
  expect([...tenantAFs.files.keys()]).toEqual(["/workspace-a/notes.txt"]);
359
316
  expect([...tenantBFs.files.keys()]).toEqual(["/workspace-b/notes.txt"]);
360
317
  expect(tenantAFs.files.get("/workspace-a/notes.txt")).toBe("alice-secret");
361
318
  expect(tenantBFs.files.get("/workspace-b/notes.txt")).toBe("bob-secret");
319
+ expect(sessionHeaders).toEqual([]);
362
320
  } finally {
363
321
  await aliceClient.close();
364
322
  await bobClient.close();
@@ -380,4 +338,25 @@ describe("remote MCP over streamable HTTP with multi-tenant in-memory fs", () =>
380
338
  await bobClient.close();
381
339
  }
382
340
  });
341
+
342
+ it("rejects a present Origin that is not allowed", async () => {
343
+ const { fetch } = createMultiTenantFetch({
344
+ baseCli: buildBaseCli(),
345
+ resolveTenant: () => {
346
+ throw new Error("tenant must not be resolved for a forbidden origin");
347
+ },
348
+ });
349
+
350
+ const response = await fetch("http://in-memory-mcp.test/mcp", {
351
+ method: "POST",
352
+ headers: {
353
+ origin: "https://evil.example",
354
+ "x-tenant-id": "tenant-a",
355
+ "content-type": "application/json",
356
+ },
357
+ body: "{}",
358
+ });
359
+
360
+ expect(response.status).toBe(403);
361
+ });
383
362
  });
package/src/auth.ts CHANGED
@@ -29,8 +29,10 @@ async function openBrowser(url: string): Promise<void> {
29
29
 
30
30
  /**
31
31
  * Start the OAuth flow for an MCP server.
32
- * This is an internal function - consumers should not call this directly.
33
- * It is automatically triggered by addMcpCommands when a 401 error occurs.
32
+ *
33
+ * Used internally by addMcpCommands on 401 errors, but also available
34
+ * for CLIs that need explicit control over the auth flow (e.g. a login
35
+ * command that runs the flow in a background daemon).
34
36
  *
35
37
  * This function:
36
38
  * 1. Starts a local callback server on a random port
package/src/cli-to-mcp.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  coerceBySchema,
21
21
  extractJsonSchema,
22
22
  GokeProcessExit,
23
+ schemaAcceptsOmittedValue,
23
24
  type Command,
24
25
  type Goke,
25
26
  type GokeExecutionContext,
@@ -503,11 +504,18 @@ function createBinding(cli: Goke, command: Command, toolName: string): CliToolBi
503
504
  }
504
505
  }
505
506
 
507
+ // `--days <days>` means "value required if the flag is present". The flag
508
+ // itself is required only when a non-optional schema rejects `undefined`.
506
509
  for (const option of options) {
507
510
  const normalized = normalizeOptionSchema(option);
508
511
  properties[option.name] = normalized.schema;
509
512
 
510
- if (option.required) {
513
+ if (
514
+ option.required
515
+ && option.default === undefined
516
+ && option.schema
517
+ && !schemaAcceptsOmittedValue(option.schema)
518
+ ) {
511
519
  requiredNames.push(option.name);
512
520
  }
513
521
 
package/src/index.ts CHANGED
@@ -9,7 +9,6 @@
9
9
  *
10
10
  * - **Auto-discovery**: Fetches all tools from the MCP server and creates CLI commands
11
11
  * - **Caching**: Tools are cached for 1 hour to avoid reconnecting on every invocation
12
- * - **Session reuse**: MCP session IDs are cached to skip initialization handshake
13
12
  * - **Type-aware parsing**: Handles string, number, boolean, object, and array arguments
14
13
  * - **JSON schema support**: Generates CLI options from tool input schemas
15
14
  * - **OAuth support**: Automatic OAuth authentication on 401 errors (lazy auth)
@@ -48,13 +47,14 @@ import { wrapJsonSchema } from "goke";
48
47
  import yaml from "js-yaml";
49
48
  import { FileOAuthProvider } from "./oauth-provider.js";
50
49
  import { startOAuthFlow, isAuthRequiredError } from "./auth.js";
50
+ export { startOAuthFlow } from "./auth.js";
51
51
  import type { McpOAuthConfig, McpOAuthState } from "./types.js";
52
52
  export { addCliToolsToMcp, createMcpAction } from "./cli-to-mcp.js";
53
53
  export type { AddCliToolsToMcpOptions, CreateMcpActionOptions } from "./cli-to-mcp.js";
54
54
 
55
- // Public exports - only types that consumers need
55
+ // Public exports
56
56
  export type { Transport };
57
- export type { McpOAuthConfig, McpOAuthState } from "./types.js";
57
+ export type { McpOAuthConfig, McpOAuthState, StartOAuthFlowOptions, OAuthFlowResult } from "./types.js";
58
58
 
59
59
  export interface CachedMcpTools {
60
60
  tools: Array<{
@@ -63,7 +63,6 @@ export interface CachedMcpTools {
63
63
  inputSchema?: unknown;
64
64
  }>;
65
65
  timestamp: number;
66
- sessionId?: string;
67
66
  }
68
67
 
69
68
  const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
@@ -91,9 +90,8 @@ export interface AddMcpCommandsOptions {
91
90
  /**
92
91
  * Returns a transport to connect to the MCP server, or null if not configured.
93
92
  * Use this for stdio servers or any setup `getMcpUrl` cannot express.
94
- * @param sessionId - Optional session ID from a still-valid cache
95
93
  */
96
- getMcpTransport?: (sessionId?: string) => Transport | null | Promise<Transport | null>;
94
+ getMcpTransport?: () => Transport | null | Promise<Transport | null>;
97
95
 
98
96
  /**
99
97
  * Argv used to decide whether to skip live discovery.
@@ -251,13 +249,11 @@ function matchesRegisteredCommand({ argv, cli }: { argv: string[]; cli: Goke })
251
249
 
252
250
  function createTransportWithAuth({
253
251
  url,
254
- sessionId,
255
252
  oauthState,
256
253
  oauth,
257
254
  headers,
258
255
  }: {
259
256
  url: URL
260
- sessionId?: string
261
257
  oauthState?: McpOAuthState
262
258
  oauth?: McpOAuthConfig
263
259
  headers?: Record<string, string>
@@ -280,7 +276,6 @@ function createTransportWithAuth({
280
276
 
281
277
  const hasHeaders = headers && Object.keys(headers).length > 0;
282
278
  return new StreamableHTTPClientTransport(url, {
283
- sessionId,
284
279
  authProvider,
285
280
  requestInit: hasHeaders ? { headers } : undefined,
286
281
  });
@@ -292,7 +287,6 @@ function createTransportWithAuth({
292
287
  * Adds MCP tool commands to a goke CLI instance.
293
288
  *
294
289
  * Tools are cached for 1 hour to avoid connecting on every CLI invocation.
295
- * Session ID is also cached to skip MCP initialization handshake.
296
290
  *
297
291
  * OAuth is lazy - authentication only happens when a 401 error occurs.
298
292
  * After successful auth, the operation is automatically retried.
@@ -311,9 +305,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
311
305
  argv = process.argv.slice(2),
312
306
  } = options;
313
307
 
314
- // Helper to get transport - supports both old and new API
315
- const getTransport = async (sessionId?: string): Promise<Transport | null> => {
316
- // New API: getMcpUrl + oauth
308
+ const getTransport = async (): Promise<Transport | null> => {
317
309
  if (getMcpUrl) {
318
310
  const mcpUrl = getMcpUrl();
319
311
  if (!mcpUrl) {
@@ -325,16 +317,14 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
325
317
 
326
318
  return createTransportWithAuth({
327
319
  url,
328
- sessionId,
329
320
  oauthState,
330
321
  oauth,
331
322
  headers: getHeaders?.(),
332
323
  });
333
324
  }
334
325
 
335
- // Custom / stdio transport
336
326
  if (getMcpTransport) {
337
- return getMcpTransport(sessionId);
327
+ return getMcpTransport();
338
328
  }
339
329
 
340
330
  return null;
@@ -376,11 +366,9 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
376
366
  isHelpOrMetaArgv(argv) || matchesRegisteredCommand({ argv, cli });
377
367
 
378
368
  let tools: CachedMcpTools["tools"] | undefined;
379
- let cachedSessionId: string | undefined;
380
369
 
381
370
  if (isCacheValid && cachedTools) {
382
371
  tools = cachedTools.tools;
383
- cachedSessionId = cachedTools.sessionId;
384
372
  } else if (skipLiveDiscovery) {
385
373
  if (cachedTools) {
386
374
  tools = cachedTools.tools;
@@ -394,8 +382,6 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
394
382
  const result = await client.listTools();
395
383
  tools = result.tools;
396
384
 
397
- const sessionId = (transport as { sessionId?: string }).sessionId;
398
-
399
385
  saveCache({
400
386
  tools: tools.map((t) => ({
401
387
  name: t.name,
@@ -403,9 +389,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
403
389
  inputSchema: t.inputSchema,
404
390
  })),
405
391
  timestamp: Date.now(),
406
- sessionId,
407
392
  });
408
- cachedSessionId = sessionId;
409
393
  } catch (err) {
410
394
  const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !skipLiveDiscovery;
411
395
  if (shouldAuth) {
@@ -480,7 +464,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
480
464
  const parsedArgs = extractToolArguments(cliOptions, inputSchema);
481
465
 
482
466
  const executeWithRetry = async (isRetry = false): Promise<void> => {
483
- const transport = await getTransport(isRetry ? undefined : cachedSessionId);
467
+ const transport = await getTransport();
484
468
  if (!transport) {
485
469
  console.error("MCP transport not available. Run login command first.");
486
470
  process.exit(1);