@goke/mcp 0.0.10 → 0.0.12

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,9 +25,9 @@ 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)
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`)
29
29
  4. **Execute** — on invocation, connects to the server and calls the tool with coerced arguments
30
- 5. **OAuth** — if the server returns 401, automatically opens the browser for OAuth, then retries
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
 
32
32
  ## Quick start
33
33
 
@@ -51,9 +51,28 @@ await addMcpCommands({
51
51
  })
52
52
 
53
53
  cli.help()
54
+ cli.completions()
54
55
  cli.parse()
55
56
  ```
56
57
 
58
+ Always pass `getMcpUrl`. Do not return `undefined` just because the user has no token. `--help` and no-args must still run so the CLI can show `config` / login instructions.
59
+
60
+ For a Bearer token, pass `getHeaders` instead of a custom transport:
61
+
62
+ ```ts
63
+ await addMcpCommands({
64
+ cli,
65
+ getMcpUrl: () => 'https://api.example.com/mcp',
66
+ getHeaders: () => {
67
+ const token = process.env.EXAMPLE_TOKEN || loadConfig().token
68
+ if (!token) return
69
+ return { Authorization: `Bearer ${token}` }
70
+ },
71
+ loadCache: () => loadConfig().cache,
72
+ saveCache: (cache) => saveConfig({ cache }),
73
+ })
74
+ ```
75
+
57
76
  That's it. Every tool the MCP server exposes becomes a CLI command:
58
77
 
59
78
  ```bash
@@ -93,6 +112,7 @@ cli.command("mcp", "Start MCP server over stdio")
93
112
  .action(createMcpAction({ cli }))
94
113
 
95
114
  cli.help()
115
+ cli.completions()
96
116
  cli.parse()
97
117
  ```
98
118
 
@@ -452,6 +472,7 @@ cli.command('logout', 'Clear tokens').action(() => {
452
472
  })
453
473
 
454
474
  cli.help()
475
+ cli.completions()
455
476
  cli.parse()
456
477
  ```
457
478
 
@@ -464,7 +485,8 @@ Registers MCP tool commands on a goke CLI instance.
464
485
  | Option | Type | Default | Description |
465
486
  |--------|------|---------|-------------|
466
487
  | `cli` | `Goke` | **required** | The goke CLI instance to add commands to |
467
- | `getMcpUrl` | `() => string \| undefined` | — | Returns the MCP server URL |
488
+ | `getMcpUrl` | `() => string \| undefined` | — | Returns the MCP server URL. Return the URL even when the user is not logged in so `--help` still works |
489
+ | `getHeaders` | `() => Record<string, string> \| undefined` | — | Extra HTTP headers (for example `Authorization`). Used with `getMcpUrl` |
468
490
  | `commandPrefix` | `string` | `''` | Prefix for commands (e.g. `'mcp'` makes `mcp notion-search`) |
469
491
  | `clientName` | `string` | `'mcp-cli-client'` | Name sent to the MCP server during connection |
470
492
  | `oauth` | `McpOAuthConfig` | — | OAuth config for servers that require authentication |
@@ -534,6 +556,8 @@ Tools and the MCP session ID are cached for **1 hour** to avoid connecting on ev
534
556
 
535
557
  When the cache expires or a tool call fails, the cache is cleared and tools are re-fetched on the next run.
536
558
 
559
+ If a live `tools/list` cannot run (no transport, 401 during `--help`), `addMcpCommands` still registers tools from an expired cache so help stays useful.
560
+
537
561
  ## License
538
562
 
539
563
  MIT
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=add-mcp-commands-help.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add-mcp-commands-help.test.d.ts","sourceRoot":"","sources":["../../src/__test__/add-mcp-commands-help.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,90 @@
1
+ // First-run help and stale-cache behavior for addMcpCommands.
2
+ import { describe, expect, it } from 'vitest';
3
+ import { goke } from 'goke';
4
+ import { addMcpCommands } from '../index.js';
5
+ const staleCache = (over = {}) => ({
6
+ tools: [
7
+ {
8
+ name: 'find_bookmarks',
9
+ description: 'Find bookmarks',
10
+ inputSchema: { type: 'object', properties: {} },
11
+ },
12
+ ],
13
+ timestamp: Date.now() - 2 * 60 * 60 * 1000,
14
+ ...over,
15
+ });
16
+ async function withArgv(argv, fn) {
17
+ const previous = process.argv;
18
+ process.argv = ['node', 'testcli', ...argv];
19
+ try {
20
+ return await fn();
21
+ }
22
+ finally {
23
+ process.argv = previous;
24
+ }
25
+ }
26
+ describe('addMcpCommands first-run help', () => {
27
+ it('lets --help run when there is no token and no cache', async () => {
28
+ const errors = [];
29
+ const error = console.error;
30
+ console.error = (...args) => {
31
+ errors.push(args.map(String).join(' '));
32
+ };
33
+ const cli = goke('testcli');
34
+ cli.command('config', 'Save token').action(() => { });
35
+ await withArgv(['--help'], async () => {
36
+ await addMcpCommands({
37
+ cli,
38
+ getMcpTransport: () => null,
39
+ loadCache: () => undefined,
40
+ saveCache: () => { },
41
+ });
42
+ });
43
+ console.error = error;
44
+ const help = cli.helpText();
45
+ expect(errors.join('\n')).not.toMatch(/Failed to connect/);
46
+ expect(help).toMatch(/config/);
47
+ expect(help).not.toMatch(/find_bookmarks/);
48
+ });
49
+ it('registers stale cached tools when live fetch is impossible', async () => {
50
+ const cli = goke('testcli');
51
+ await withArgv(['--help'], async () => {
52
+ await addMcpCommands({
53
+ cli,
54
+ getMcpTransport: () => null,
55
+ loadCache: () => staleCache(),
56
+ saveCache: () => { },
57
+ });
58
+ });
59
+ expect(cli.helpText()).toMatch(/find_bookmarks/);
60
+ });
61
+ it('does not start OAuth when --help gets a 401', async () => {
62
+ const authUrls = [];
63
+ const errors = [];
64
+ const error = console.error;
65
+ console.error = (...args) => {
66
+ errors.push(args.map(String).join(' '));
67
+ };
68
+ const cli = goke('testcli');
69
+ await withArgv(['--help'], async () => {
70
+ await addMcpCommands({
71
+ cli,
72
+ getMcpUrl: () => 'http://127.0.0.1:1/mcp',
73
+ oauth: {
74
+ clientName: 'test',
75
+ load: () => undefined,
76
+ save: () => { },
77
+ onAuthUrl: (url) => {
78
+ authUrls.push(url);
79
+ },
80
+ },
81
+ loadCache: () => undefined,
82
+ saveCache: () => { },
83
+ });
84
+ });
85
+ console.error = error;
86
+ expect(authUrls).toEqual([]);
87
+ expect(errors.join('\n')).not.toMatch(/Authentication required/);
88
+ expect(cli.helpText()).toMatch(/Usage/);
89
+ });
90
+ });
package/dist/index.d.ts CHANGED
@@ -81,6 +81,11 @@ export interface AddMcpCommandsOptions {
81
81
  * @deprecated Use getMcpUrl + oauth instead for simpler setup
82
82
  */
83
83
  getMcpTransport?: (sessionId?: string) => Transport | null | Promise<Transport | null>;
84
+ /**
85
+ * Extra headers for MCP HTTP requests (for example `Authorization`).
86
+ * Used with `getMcpUrl`. Ignored when `getMcpTransport` is set.
87
+ */
88
+ getHeaders?: () => Record<string, string> | undefined;
84
89
  /**
85
90
  * OAuth configuration. When provided, enables automatic OAuth authentication.
86
91
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAKjC,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACpE,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAGvF,YAAY,EAAE,SAAS,EAAE,CAAC;AAC1B,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB,CAAC,CAAC;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,IAAI,CAAC;IACV;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAErC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAEvF;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC;IAEvB;;OAEG;IACH,SAAS,EAAE,MAAM,cAAc,GAAG,SAAS,CAAC;IAE5C;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,KAAK,IAAI,CAAC;CACxD;AAmID;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyMlF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAKjC,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACpE,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAGvF,YAAY,EAAE,SAAS,EAAE,CAAC;AAC1B,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB,CAAC,CAAC;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,IAAI,CAAC;IACV;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAErC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAEvF;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAEtD;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC;IAEvB;;OAEG;IACH,SAAS,EAAE,MAAM,cAAc,GAAG,SAAS,CAAC;IAE5C;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,KAAK,IAAI,CAAC;CACxD;AA4ID;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiNlF"}
package/dist/index.js CHANGED
@@ -124,7 +124,14 @@ function outputResult(result) {
124
124
  /**
125
125
  * Create a transport with optional OAuth authentication
126
126
  */
127
- function createTransportWithAuth(url, sessionId, oauthState, oauth) {
127
+ function isHelpOrMetaArgv(argv) {
128
+ if (argv.length === 0)
129
+ return true;
130
+ if (argv[0] === "completions" || argv.includes("--get-goke-completions"))
131
+ return true;
132
+ return argv.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v");
133
+ }
134
+ function createTransportWithAuth(url, sessionId, oauthState, oauth, headers) {
128
135
  let authProvider;
129
136
  if (oauth && oauthState?.tokens) {
130
137
  authProvider = new FileOAuthProvider({
@@ -139,9 +146,11 @@ function createTransportWithAuth(url, sessionId, oauthState, oauth) {
139
146
  },
140
147
  });
141
148
  }
149
+ const hasHeaders = headers && Object.keys(headers).length > 0;
142
150
  return new StreamableHTTPClientTransport(url, {
143
151
  sessionId,
144
152
  authProvider,
153
+ requestInit: hasHeaders ? { headers } : undefined,
145
154
  });
146
155
  }
147
156
  /**
@@ -154,7 +163,7 @@ function createTransportWithAuth(url, sessionId, oauthState, oauth) {
154
163
  * After successful auth, the operation is automatically retried.
155
164
  */
156
165
  export async function addMcpCommands(options) {
157
- const { cli, commandPrefix = "", clientName = "mcp-cli-client", getMcpUrl, getMcpTransport, oauth, loadCache, saveCache, } = options;
166
+ const { cli, commandPrefix = "", clientName = "mcp-cli-client", getMcpUrl, getMcpTransport, getHeaders, oauth, loadCache, saveCache, } = options;
158
167
  // Helper to get transport - supports both old and new API
159
168
  const getTransport = async (sessionId) => {
160
169
  // New API: getMcpUrl + oauth
@@ -165,7 +174,7 @@ export async function addMcpCommands(options) {
165
174
  }
166
175
  const url = new URL(mcpUrl);
167
176
  const oauthState = oauth?.load();
168
- return createTransportWithAuth(url, sessionId, oauthState, oauth);
177
+ return createTransportWithAuth(url, sessionId, oauthState, oauth, getHeaders?.());
169
178
  }
170
179
  // Legacy API: getMcpTransport
171
180
  if (getMcpTransport) {
@@ -200,54 +209,60 @@ export async function addMcpCommands(options) {
200
209
  // Try to use cached tools first (fast path - no network)
201
210
  const cachedTools = loadCache();
202
211
  const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
212
+ const helpOrMeta = isHelpOrMetaArgv(process.argv.slice(2));
203
213
  let tools;
204
214
  let cachedSessionId;
205
- if (isCacheValid) {
215
+ if (isCacheValid && cachedTools) {
206
216
  tools = cachedTools.tools;
207
217
  cachedSessionId = cachedTools.sessionId;
208
218
  }
209
219
  else {
210
- // Cache invalid/missing - connect to fetch tools
211
220
  const transport = await getTransport();
212
- if (!transport) {
213
- return;
214
- }
215
- const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
216
- try {
217
- await client.connect(transport);
218
- const result = await client.listTools();
219
- tools = result.tools;
220
- const sessionId = transport.sessionId;
221
- saveCache({
222
- tools: tools.map((t) => ({
223
- name: t.name,
224
- description: t.description,
225
- inputSchema: t.inputSchema,
226
- })),
227
- timestamp: Date.now(),
228
- sessionId,
229
- });
230
- cachedSessionId = sessionId;
231
- }
232
- catch (err) {
233
- // Check if auth is required during tool discovery
234
- if (isAuthRequiredError(err) && oauth && getMcpUrl) {
235
- const mcpUrl = getMcpUrl();
236
- if (mcpUrl) {
237
- const authSuccess = await handleAuthRequired((mcpUrl).toString());
238
- if (authSuccess) {
239
- // Retry after auth
240
- return addMcpCommands(options);
221
+ if (transport) {
222
+ const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
223
+ try {
224
+ await client.connect(transport);
225
+ const result = await client.listTools();
226
+ tools = result.tools;
227
+ const sessionId = transport.sessionId;
228
+ saveCache({
229
+ tools: tools.map((t) => ({
230
+ name: t.name,
231
+ description: t.description,
232
+ inputSchema: t.inputSchema,
233
+ })),
234
+ timestamp: Date.now(),
235
+ sessionId,
236
+ });
237
+ cachedSessionId = sessionId;
238
+ }
239
+ catch (err) {
240
+ const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !helpOrMeta;
241
+ if (shouldAuth) {
242
+ const mcpUrl = getMcpUrl();
243
+ if (mcpUrl) {
244
+ const authSuccess = await handleAuthRequired(mcpUrl);
245
+ if (authSuccess) {
246
+ return addMcpCommands(options);
247
+ }
241
248
  }
242
249
  }
250
+ if (!helpOrMeta) {
251
+ console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
252
+ }
253
+ }
254
+ finally {
255
+ await client.close();
243
256
  }
244
- console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
245
- return;
246
257
  }
247
- finally {
248
- await client.close();
258
+ if (!tools && cachedTools) {
259
+ tools = cachedTools.tools;
260
+ cachedSessionId = cachedTools.sessionId;
249
261
  }
250
262
  }
263
+ if (!tools) {
264
+ return;
265
+ }
251
266
  // Register CLI commands for each tool
252
267
  for (const tool of tools) {
253
268
  const inputSchema = tool.inputSchema;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goke/mcp",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "description": "Dynamically generate CLI commands from MCP server tools",
6
6
  "repository": {
@@ -51,7 +51,7 @@
51
51
  "@types/node": "^22.19.7",
52
52
  "vitest": "^3.1.0",
53
53
  "zod": "^4.3.6",
54
- "goke": "^6.8.0"
54
+ "goke": "^6.12.1"
55
55
  },
56
56
  "scripts": {
57
57
  "clean": "rm -rf dist",
@@ -0,0 +1,102 @@
1
+ // First-run help and stale-cache behavior for addMcpCommands.
2
+ import { describe, expect, it } from 'vitest'
3
+ import { goke } from 'goke'
4
+ import { addMcpCommands, type CachedMcpTools } from '../index.js'
5
+
6
+ const staleCache = (over: Partial<CachedMcpTools> = {}): CachedMcpTools => ({
7
+ tools: [
8
+ {
9
+ name: 'find_bookmarks',
10
+ description: 'Find bookmarks',
11
+ inputSchema: { type: 'object', properties: {} },
12
+ },
13
+ ],
14
+ timestamp: Date.now() - 2 * 60 * 60 * 1000,
15
+ ...over,
16
+ })
17
+
18
+ async function withArgv<T>(argv: string[], fn: () => Promise<T>) {
19
+ const previous = process.argv
20
+ process.argv = ['node', 'testcli', ...argv]
21
+ try {
22
+ return await fn()
23
+ } finally {
24
+ process.argv = previous
25
+ }
26
+ }
27
+
28
+ describe('addMcpCommands first-run help', () => {
29
+ it('lets --help run when there is no token and no cache', async () => {
30
+ const errors: string[] = []
31
+ const error = console.error
32
+ console.error = (...args: unknown[]) => {
33
+ errors.push(args.map(String).join(' '))
34
+ }
35
+
36
+ const cli = goke('testcli')
37
+ cli.command('config', 'Save token').action(() => {})
38
+
39
+ await withArgv(['--help'], async () => {
40
+ await addMcpCommands({
41
+ cli,
42
+ getMcpTransport: () => null,
43
+ loadCache: () => undefined,
44
+ saveCache: () => {},
45
+ })
46
+ })
47
+ console.error = error
48
+
49
+ const help = cli.helpText()
50
+ expect(errors.join('\n')).not.toMatch(/Failed to connect/)
51
+ expect(help).toMatch(/config/)
52
+ expect(help).not.toMatch(/find_bookmarks/)
53
+ })
54
+
55
+ it('registers stale cached tools when live fetch is impossible', async () => {
56
+ const cli = goke('testcli')
57
+
58
+ await withArgv(['--help'], async () => {
59
+ await addMcpCommands({
60
+ cli,
61
+ getMcpTransport: () => null,
62
+ loadCache: () => staleCache(),
63
+ saveCache: () => {},
64
+ })
65
+ })
66
+
67
+ expect(cli.helpText()).toMatch(/find_bookmarks/)
68
+ })
69
+
70
+ it('does not start OAuth when --help gets a 401', async () => {
71
+ const authUrls: string[] = []
72
+ const errors: string[] = []
73
+ const error = console.error
74
+ console.error = (...args: unknown[]) => {
75
+ errors.push(args.map(String).join(' '))
76
+ }
77
+
78
+ const cli = goke('testcli')
79
+
80
+ await withArgv(['--help'], async () => {
81
+ await addMcpCommands({
82
+ cli,
83
+ getMcpUrl: () => 'http://127.0.0.1:1/mcp',
84
+ oauth: {
85
+ clientName: 'test',
86
+ load: () => undefined,
87
+ save: () => {},
88
+ onAuthUrl: (url) => {
89
+ authUrls.push(url)
90
+ },
91
+ },
92
+ loadCache: () => undefined,
93
+ saveCache: () => {},
94
+ })
95
+ })
96
+ console.error = error
97
+
98
+ expect(authUrls).toEqual([])
99
+ expect(errors.join('\n')).not.toMatch(/Authentication required/)
100
+ expect(cli.helpText()).toMatch(/Usage/)
101
+ })
102
+ })
package/src/index.ts CHANGED
@@ -97,6 +97,12 @@ export interface AddMcpCommandsOptions {
97
97
  */
98
98
  getMcpTransport?: (sessionId?: string) => Transport | null | Promise<Transport | null>;
99
99
 
100
+ /**
101
+ * Extra headers for MCP HTTP requests (for example `Authorization`).
102
+ * Used with `getMcpUrl`. Ignored when `getMcpTransport` is set.
103
+ */
104
+ getHeaders?: () => Record<string, string> | undefined;
105
+
100
106
  /**
101
107
  * OAuth configuration. When provided, enables automatic OAuth authentication.
102
108
  *
@@ -224,11 +230,18 @@ function outputResult(result: {
224
230
  /**
225
231
  * Create a transport with optional OAuth authentication
226
232
  */
233
+ function isHelpOrMetaArgv(argv: string[]) {
234
+ if (argv.length === 0) return true;
235
+ if (argv[0] === "completions" || argv.includes("--get-goke-completions")) return true;
236
+ return argv.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v");
237
+ }
238
+
227
239
  function createTransportWithAuth(
228
240
  url: URL,
229
241
  sessionId: string | undefined,
230
242
  oauthState: McpOAuthState | undefined,
231
243
  oauth: McpOAuthConfig | undefined,
244
+ headers?: Record<string, string>,
232
245
  ): StreamableHTTPClientTransport {
233
246
  let authProvider: FileOAuthProvider | undefined;
234
247
 
@@ -246,9 +259,11 @@ function createTransportWithAuth(
246
259
  });
247
260
  }
248
261
 
262
+ const hasHeaders = headers && Object.keys(headers).length > 0;
249
263
  return new StreamableHTTPClientTransport(url, {
250
264
  sessionId,
251
265
  authProvider,
266
+ requestInit: hasHeaders ? { headers } : undefined,
252
267
  });
253
268
  }
254
269
 
@@ -270,6 +285,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
270
285
  clientName = "mcp-cli-client",
271
286
  getMcpUrl,
272
287
  getMcpTransport,
288
+ getHeaders,
273
289
  oauth,
274
290
  loadCache,
275
291
  saveCache,
@@ -287,7 +303,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
287
303
  const url = new URL(mcpUrl);
288
304
  const oauthState = oauth?.load();
289
305
 
290
- return createTransportWithAuth(url, sessionId, oauthState, oauth);
306
+ return createTransportWithAuth(url, sessionId, oauthState, oauth, getHeaders?.());
291
307
  }
292
308
 
293
309
  // Legacy API: getMcpTransport
@@ -330,55 +346,62 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
330
346
  // Try to use cached tools first (fast path - no network)
331
347
  const cachedTools = loadCache();
332
348
  const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
349
+ const helpOrMeta = isHelpOrMetaArgv(process.argv.slice(2));
333
350
 
334
- let tools: CachedMcpTools["tools"];
351
+ let tools: CachedMcpTools["tools"] | undefined;
335
352
  let cachedSessionId: string | undefined;
336
353
 
337
- if (isCacheValid) {
354
+ if (isCacheValid && cachedTools) {
338
355
  tools = cachedTools.tools;
339
356
  cachedSessionId = cachedTools.sessionId;
340
357
  } else {
341
- // Cache invalid/missing - connect to fetch tools
342
358
  const transport = await getTransport();
343
- if (!transport) {
344
- return;
345
- }
346
-
347
- const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
348
- try {
349
- await client.connect(transport);
350
- const result = await client.listTools();
351
- tools = result.tools;
352
-
353
- const sessionId = (transport as { sessionId?: string }).sessionId;
354
-
355
- saveCache({
356
- tools: tools.map((t) => ({
357
- name: t.name,
358
- description: t.description,
359
- inputSchema: t.inputSchema,
360
- })),
361
- timestamp: Date.now(),
362
- sessionId,
363
- });
364
- cachedSessionId = sessionId;
365
- } catch (err) {
366
- // Check if auth is required during tool discovery
367
- if (isAuthRequiredError(err) && oauth && getMcpUrl) {
368
- const mcpUrl = getMcpUrl();
369
- if (mcpUrl) {
370
- const authSuccess = await handleAuthRequired((mcpUrl).toString());
371
- if (authSuccess) {
372
- // Retry after auth
373
- return addMcpCommands(options);
359
+ if (transport) {
360
+ const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
361
+ try {
362
+ await client.connect(transport);
363
+ const result = await client.listTools();
364
+ tools = result.tools;
365
+
366
+ const sessionId = (transport as { sessionId?: string }).sessionId;
367
+
368
+ saveCache({
369
+ tools: tools.map((t) => ({
370
+ name: t.name,
371
+ description: t.description,
372
+ inputSchema: t.inputSchema,
373
+ })),
374
+ timestamp: Date.now(),
375
+ sessionId,
376
+ });
377
+ cachedSessionId = sessionId;
378
+ } catch (err) {
379
+ const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !helpOrMeta;
380
+ if (shouldAuth) {
381
+ const mcpUrl = getMcpUrl();
382
+ if (mcpUrl) {
383
+ const authSuccess = await handleAuthRequired(mcpUrl);
384
+ if (authSuccess) {
385
+ return addMcpCommands(options);
386
+ }
374
387
  }
375
388
  }
389
+ if (!helpOrMeta) {
390
+ console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
391
+ }
392
+ } finally {
393
+ await client.close();
376
394
  }
377
- console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
378
- return;
379
- } finally {
380
- await client.close();
381
395
  }
396
+
397
+ if (!tools && cachedTools) {
398
+ tools = cachedTools.tools;
399
+ cachedSessionId = cachedTools.sessionId;
400
+ }
401
+ }
402
+
403
+ if (!tools) {
404
+ return;
382
405
  }
383
406
 
384
407
  // Register CLI commands for each tool