@goke/mcp 0.0.11 → 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
 
@@ -55,6 +55,24 @@ cli.completions()
55
55
  cli.parse()
56
56
  ```
57
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
+
58
76
  That's it. Every tool the MCP server exposes becomes a CLI command:
59
77
 
60
78
  ```bash
@@ -467,7 +485,8 @@ Registers MCP tool commands on a goke CLI instance.
467
485
  | Option | Type | Default | Description |
468
486
  |--------|------|---------|-------------|
469
487
  | `cli` | `Goke` | **required** | The goke CLI instance to add commands to |
470
- | `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` |
471
490
  | `commandPrefix` | `string` | `''` | Prefix for commands (e.g. `'mcp'` makes `mcp notion-search`) |
472
491
  | `clientName` | `string` | `'mcp-cli-client'` | Name sent to the MCP server during connection |
473
492
  | `oauth` | `McpOAuthConfig` | — | OAuth config for servers that require authentication |
@@ -537,6 +556,8 @@ Tools and the MCP session ID are cached for **1 hour** to avoid connecting on ev
537
556
 
538
557
  When the cache expires or a tool call fails, the cache is cleared and tools are re-fetched on the next run.
539
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
+
540
561
  ## License
541
562
 
542
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/auth.d.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import type { OAuthFlowResult, StartOAuthFlowOptions } from "./types.js";
2
2
  /**
3
3
  * Start the OAuth flow for an MCP server.
4
- *
5
- * Used internally by addMcpCommands on 401 errors, but also available
6
- * for CLIs that need explicit control over the auth flow (e.g. a login
7
- * command that runs the flow in a background daemon).
4
+ * This is an internal function - consumers should not call this directly.
5
+ * It is automatically triggered by addMcpCommands when a 401 error occurs.
8
6
  *
9
7
  * This function:
10
8
  * 1. Starts a local callback server on a random port
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,eAAe,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AA0BxF;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4E7F;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAazD"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,eAAe,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AA0BxF;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4E7F;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAazD"}
package/dist/auth.js CHANGED
@@ -24,10 +24,8 @@ async function openBrowser(url) {
24
24
  }
25
25
  /**
26
26
  * Start the OAuth flow for an MCP server.
27
- *
28
- * Used internally by addMcpCommands on 401 errors, but also available
29
- * for CLIs that need explicit control over the auth flow (e.g. a login
30
- * command that runs the flow in a background daemon).
27
+ * This is an internal function - consumers should not call this directly.
28
+ * It is automatically triggered by addMcpCommands when a 401 error occurs.
31
29
  *
32
30
  * This function:
33
31
  * 1. Starts a local callback server on a random port
package/dist/index.d.ts CHANGED
@@ -41,12 +41,11 @@
41
41
  */
42
42
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
43
43
  import type { Goke } from "goke";
44
- export { startOAuthFlow } from "./auth.js";
45
44
  import type { McpOAuthConfig } from "./types.js";
46
45
  export { addCliToolsToMcp, createMcpAction } from "./cli-to-mcp.js";
47
46
  export type { AddCliToolsToMcpOptions, CreateMcpActionOptions } from "./cli-to-mcp.js";
48
47
  export type { Transport };
49
- export type { McpOAuthConfig, McpOAuthState, StartOAuthFlowOptions, OAuthFlowResult } from "./types.js";
48
+ export type { McpOAuthConfig, McpOAuthState } from "./types.js";
50
49
  export interface CachedMcpTools {
51
50
  tools: Array<{
52
51
  name: string;
@@ -82,6 +81,11 @@ export interface AddMcpCommandsOptions {
82
81
  * @deprecated Use getMcpUrl + oauth instead for simpler setup
83
82
  */
84
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;
85
89
  /**
86
90
  * OAuth configuration. When provided, enables automatic OAuth authentication.
87
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,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,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,qBAAqB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAExG,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
@@ -45,7 +45,6 @@ import { wrapJsonSchema } from "goke";
45
45
  import yaml from "js-yaml";
46
46
  import { FileOAuthProvider } from "./oauth-provider.js";
47
47
  import { startOAuthFlow, isAuthRequiredError } from "./auth.js";
48
- export { startOAuthFlow } from "./auth.js";
49
48
  export { addCliToolsToMcp, createMcpAction } from "./cli-to-mcp.js";
50
49
  const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
51
50
  /**
@@ -125,7 +124,14 @@ function outputResult(result) {
125
124
  /**
126
125
  * Create a transport with optional OAuth authentication
127
126
  */
128
- 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) {
129
135
  let authProvider;
130
136
  if (oauth && oauthState?.tokens) {
131
137
  authProvider = new FileOAuthProvider({
@@ -140,9 +146,11 @@ function createTransportWithAuth(url, sessionId, oauthState, oauth) {
140
146
  },
141
147
  });
142
148
  }
149
+ const hasHeaders = headers && Object.keys(headers).length > 0;
143
150
  return new StreamableHTTPClientTransport(url, {
144
151
  sessionId,
145
152
  authProvider,
153
+ requestInit: hasHeaders ? { headers } : undefined,
146
154
  });
147
155
  }
148
156
  /**
@@ -155,7 +163,7 @@ function createTransportWithAuth(url, sessionId, oauthState, oauth) {
155
163
  * After successful auth, the operation is automatically retried.
156
164
  */
157
165
  export async function addMcpCommands(options) {
158
- 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;
159
167
  // Helper to get transport - supports both old and new API
160
168
  const getTransport = async (sessionId) => {
161
169
  // New API: getMcpUrl + oauth
@@ -166,7 +174,7 @@ export async function addMcpCommands(options) {
166
174
  }
167
175
  const url = new URL(mcpUrl);
168
176
  const oauthState = oauth?.load();
169
- return createTransportWithAuth(url, sessionId, oauthState, oauth);
177
+ return createTransportWithAuth(url, sessionId, oauthState, oauth, getHeaders?.());
170
178
  }
171
179
  // Legacy API: getMcpTransport
172
180
  if (getMcpTransport) {
@@ -201,54 +209,60 @@ export async function addMcpCommands(options) {
201
209
  // Try to use cached tools first (fast path - no network)
202
210
  const cachedTools = loadCache();
203
211
  const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
212
+ const helpOrMeta = isHelpOrMetaArgv(process.argv.slice(2));
204
213
  let tools;
205
214
  let cachedSessionId;
206
- if (isCacheValid) {
215
+ if (isCacheValid && cachedTools) {
207
216
  tools = cachedTools.tools;
208
217
  cachedSessionId = cachedTools.sessionId;
209
218
  }
210
219
  else {
211
- // Cache invalid/missing - connect to fetch tools
212
220
  const transport = await getTransport();
213
- if (!transport) {
214
- return;
215
- }
216
- const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
217
- try {
218
- await client.connect(transport);
219
- const result = await client.listTools();
220
- tools = result.tools;
221
- const sessionId = transport.sessionId;
222
- saveCache({
223
- tools: tools.map((t) => ({
224
- name: t.name,
225
- description: t.description,
226
- inputSchema: t.inputSchema,
227
- })),
228
- timestamp: Date.now(),
229
- sessionId,
230
- });
231
- cachedSessionId = sessionId;
232
- }
233
- catch (err) {
234
- // Check if auth is required during tool discovery
235
- if (isAuthRequiredError(err) && oauth && getMcpUrl) {
236
- const mcpUrl = getMcpUrl();
237
- if (mcpUrl) {
238
- const authSuccess = await handleAuthRequired((mcpUrl).toString());
239
- if (authSuccess) {
240
- // Retry after auth
241
- 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
+ }
242
248
  }
243
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();
244
256
  }
245
- console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
246
- return;
247
257
  }
248
- finally {
249
- await client.close();
258
+ if (!tools && cachedTools) {
259
+ tools = cachedTools.tools;
260
+ cachedSessionId = cachedTools.sessionId;
250
261
  }
251
262
  }
263
+ if (!tools) {
264
+ return;
265
+ }
252
266
  // Register CLI commands for each tool
253
267
  for (const tool of tools) {
254
268
  const inputSchema = tool.inputSchema;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goke/mcp",
3
- "version": "0.0.11",
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.13.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/auth.ts CHANGED
@@ -29,10 +29,8 @@ async function openBrowser(url: string): Promise<void> {
29
29
 
30
30
  /**
31
31
  * Start the OAuth flow for an MCP server.
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).
32
+ * This is an internal function - consumers should not call this directly.
33
+ * It is automatically triggered by addMcpCommands when a 401 error occurs.
36
34
  *
37
35
  * This function:
38
36
  * 1. Starts a local callback server on a random port
package/src/index.ts CHANGED
@@ -48,14 +48,13 @@ import { wrapJsonSchema } from "goke";
48
48
  import yaml from "js-yaml";
49
49
  import { FileOAuthProvider } from "./oauth-provider.js";
50
50
  import { startOAuthFlow, isAuthRequiredError } from "./auth.js";
51
- export { startOAuthFlow } from "./auth.js";
52
51
  import type { McpOAuthConfig, McpOAuthState } from "./types.js";
53
52
  export { addCliToolsToMcp, createMcpAction } from "./cli-to-mcp.js";
54
53
  export type { AddCliToolsToMcpOptions, CreateMcpActionOptions } from "./cli-to-mcp.js";
55
54
 
56
- // Public exports
55
+ // Public exports - only types that consumers need
57
56
  export type { Transport };
58
- export type { McpOAuthConfig, McpOAuthState, StartOAuthFlowOptions, OAuthFlowResult } from "./types.js";
57
+ export type { McpOAuthConfig, McpOAuthState } from "./types.js";
59
58
 
60
59
  export interface CachedMcpTools {
61
60
  tools: Array<{
@@ -98,6 +97,12 @@ export interface AddMcpCommandsOptions {
98
97
  */
99
98
  getMcpTransport?: (sessionId?: string) => Transport | null | Promise<Transport | null>;
100
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
+
101
106
  /**
102
107
  * OAuth configuration. When provided, enables automatic OAuth authentication.
103
108
  *
@@ -225,11 +230,18 @@ function outputResult(result: {
225
230
  /**
226
231
  * Create a transport with optional OAuth authentication
227
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
+
228
239
  function createTransportWithAuth(
229
240
  url: URL,
230
241
  sessionId: string | undefined,
231
242
  oauthState: McpOAuthState | undefined,
232
243
  oauth: McpOAuthConfig | undefined,
244
+ headers?: Record<string, string>,
233
245
  ): StreamableHTTPClientTransport {
234
246
  let authProvider: FileOAuthProvider | undefined;
235
247
 
@@ -247,9 +259,11 @@ function createTransportWithAuth(
247
259
  });
248
260
  }
249
261
 
262
+ const hasHeaders = headers && Object.keys(headers).length > 0;
250
263
  return new StreamableHTTPClientTransport(url, {
251
264
  sessionId,
252
265
  authProvider,
266
+ requestInit: hasHeaders ? { headers } : undefined,
253
267
  });
254
268
  }
255
269
 
@@ -271,6 +285,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
271
285
  clientName = "mcp-cli-client",
272
286
  getMcpUrl,
273
287
  getMcpTransport,
288
+ getHeaders,
274
289
  oauth,
275
290
  loadCache,
276
291
  saveCache,
@@ -288,7 +303,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
288
303
  const url = new URL(mcpUrl);
289
304
  const oauthState = oauth?.load();
290
305
 
291
- return createTransportWithAuth(url, sessionId, oauthState, oauth);
306
+ return createTransportWithAuth(url, sessionId, oauthState, oauth, getHeaders?.());
292
307
  }
293
308
 
294
309
  // Legacy API: getMcpTransport
@@ -331,55 +346,62 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
331
346
  // Try to use cached tools first (fast path - no network)
332
347
  const cachedTools = loadCache();
333
348
  const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
349
+ const helpOrMeta = isHelpOrMetaArgv(process.argv.slice(2));
334
350
 
335
- let tools: CachedMcpTools["tools"];
351
+ let tools: CachedMcpTools["tools"] | undefined;
336
352
  let cachedSessionId: string | undefined;
337
353
 
338
- if (isCacheValid) {
354
+ if (isCacheValid && cachedTools) {
339
355
  tools = cachedTools.tools;
340
356
  cachedSessionId = cachedTools.sessionId;
341
357
  } else {
342
- // Cache invalid/missing - connect to fetch tools
343
358
  const transport = await getTransport();
344
- if (!transport) {
345
- return;
346
- }
347
-
348
- const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
349
- try {
350
- await client.connect(transport);
351
- const result = await client.listTools();
352
- tools = result.tools;
353
-
354
- const sessionId = (transport as { sessionId?: string }).sessionId;
355
-
356
- saveCache({
357
- tools: tools.map((t) => ({
358
- name: t.name,
359
- description: t.description,
360
- inputSchema: t.inputSchema,
361
- })),
362
- timestamp: Date.now(),
363
- sessionId,
364
- });
365
- cachedSessionId = sessionId;
366
- } catch (err) {
367
- // Check if auth is required during tool discovery
368
- if (isAuthRequiredError(err) && oauth && getMcpUrl) {
369
- const mcpUrl = getMcpUrl();
370
- if (mcpUrl) {
371
- const authSuccess = await handleAuthRequired((mcpUrl).toString());
372
- if (authSuccess) {
373
- // Retry after auth
374
- 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
+ }
375
387
  }
376
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();
377
394
  }
378
- console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
379
- return;
380
- } finally {
381
- await client.close();
382
395
  }
396
+
397
+ if (!tools && cachedTools) {
398
+ tools = cachedTools.tools;
399
+ cachedSessionId = cachedTools.sessionId;
400
+ }
401
+ }
402
+
403
+ if (!tools) {
404
+ return;
383
405
  }
384
406
 
385
407
  // Register CLI commands for each tool