@goke/mcp 0.0.12 → 0.0.13

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
@@ -55,9 +55,9 @@ 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.
58
+ If the MCP server is HTTP, pass `getMcpUrl` even when the user has no token. `--help` and no-args must still run so the CLI can show `config` / login instructions. Use `getMcpTransport` when you need stdio or a custom transport.
59
59
 
60
- For a Bearer token, pass `getHeaders` instead of a custom transport:
60
+ For an HTTP Bearer token, pass `getHeaders`:
61
61
 
62
62
  ```ts
63
63
  await addMcpCommands({
@@ -486,7 +486,9 @@ Registers MCP tool commands on a goke CLI instance.
486
486
  |--------|------|---------|-------------|
487
487
  | `cli` | `Goke` | **required** | The goke CLI instance to add commands to |
488
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
+ | `getMcpTransport` | `(sessionId?) => Transport \| null` | — | Custom transport. Use for stdio or anything `getMcpUrl` cannot express |
489
490
  | `getHeaders` | `() => Record<string, string> \| undefined` | — | Extra HTTP headers (for example `Authorization`). Used with `getMcpUrl` |
491
+ | `argv` | `string[]` | `process.argv.slice(2)` | Args used to skip live discovery on help and already registered commands |
490
492
  | `commandPrefix` | `string` | `''` | Prefix for commands (e.g. `'mcp'` makes `mcp notion-search`) |
491
493
  | `clientName` | `string` | `'mcp-cli-client'` | Name sent to the MCP server during connection |
492
494
  | `oauth` | `McpOAuthConfig` | — | OAuth config for servers that require authentication |
@@ -1,4 +1,5 @@
1
1
  // First-run help and stale-cache behavior for addMcpCommands.
2
+ import http from 'node:http';
2
3
  import { describe, expect, it } from 'vitest';
3
4
  import { goke } from 'goke';
4
5
  import { addMcpCommands } from '../index.js';
@@ -11,65 +12,82 @@ const staleCache = (over = {}) => ({
11
12
  },
12
13
  ],
13
14
  timestamp: Date.now() - 2 * 60 * 60 * 1000,
15
+ sessionId: 'stale-session',
14
16
  ...over,
15
17
  });
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
- }
18
+ function captureErrors() {
19
+ const errors = [];
20
+ const error = console.error;
21
+ console.error = (...args) => {
22
+ errors.push(args.map(String).join(' '));
23
+ };
24
+ return {
25
+ errors,
26
+ restore() {
27
+ console.error = error;
28
+ },
29
+ };
30
+ }
31
+ function listen401() {
32
+ const server = http.createServer((_req, res) => {
33
+ res.writeHead(401, { 'content-type': 'application/json' });
34
+ res.end(JSON.stringify({ error: 'unauthorized' }));
35
+ });
36
+ return new Promise((resolve) => {
37
+ server.listen(0, '127.0.0.1', () => {
38
+ const addr = server.address();
39
+ if (!addr || typeof addr === 'string')
40
+ throw new Error('no port');
41
+ resolve({
42
+ url: `http://127.0.0.1:${addr.port}/mcp`,
43
+ close: () => new Promise((done) => server.close(() => done())),
44
+ });
45
+ });
46
+ });
25
47
  }
26
48
  describe('addMcpCommands first-run help', () => {
27
49
  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
- };
50
+ const io = captureErrors();
33
51
  const cli = goke('testcli');
34
52
  cli.command('config', 'Save token').action(() => { });
35
- await withArgv(['--help'], async () => {
53
+ try {
36
54
  await addMcpCommands({
37
55
  cli,
56
+ argv: ['--help'],
38
57
  getMcpTransport: () => null,
39
58
  loadCache: () => undefined,
40
59
  saveCache: () => { },
41
60
  });
42
- });
43
- console.error = error;
61
+ }
62
+ finally {
63
+ io.restore();
64
+ }
44
65
  const help = cli.helpText();
45
- expect(errors.join('\n')).not.toMatch(/Failed to connect/);
66
+ expect(io.errors.join('\n')).not.toMatch(/Failed to connect/);
46
67
  expect(help).toMatch(/config/);
47
68
  expect(help).not.toMatch(/find_bookmarks/);
48
69
  });
49
70
  it('registers stale cached tools when live fetch is impossible', async () => {
50
71
  const cli = goke('testcli');
51
- await withArgv(['--help'], async () => {
52
- await addMcpCommands({
53
- cli,
54
- getMcpTransport: () => null,
55
- loadCache: () => staleCache(),
56
- saveCache: () => { },
57
- });
72
+ await addMcpCommands({
73
+ cli,
74
+ argv: ['--help'],
75
+ getMcpTransport: () => null,
76
+ loadCache: () => staleCache(),
77
+ saveCache: () => { },
58
78
  });
59
79
  expect(cli.helpText()).toMatch(/find_bookmarks/);
60
80
  });
61
81
  it('does not start OAuth when --help gets a 401', async () => {
82
+ const server = await listen401();
62
83
  const authUrls = [];
63
- const errors = [];
64
- const error = console.error;
65
- console.error = (...args) => {
66
- errors.push(args.map(String).join(' '));
67
- };
84
+ const io = captureErrors();
68
85
  const cli = goke('testcli');
69
- await withArgv(['--help'], async () => {
86
+ try {
70
87
  await addMcpCommands({
71
88
  cli,
72
- getMcpUrl: () => 'http://127.0.0.1:1/mcp',
89
+ argv: ['--help'],
90
+ getMcpUrl: () => server.url,
73
91
  oauth: {
74
92
  clientName: 'test',
75
93
  load: () => undefined,
@@ -81,10 +99,29 @@ describe('addMcpCommands first-run help', () => {
81
99
  loadCache: () => undefined,
82
100
  saveCache: () => { },
83
101
  });
84
- });
85
- console.error = error;
102
+ }
103
+ finally {
104
+ io.restore();
105
+ await server.close();
106
+ }
86
107
  expect(authUrls).toEqual([]);
87
- expect(errors.join('\n')).not.toMatch(/Authentication required/);
108
+ expect(io.errors.join('\n')).not.toMatch(/Authentication required/);
88
109
  expect(cli.helpText()).toMatch(/Usage/);
89
110
  });
111
+ it('does not connect for an already registered command', async () => {
112
+ let transportCalls = 0;
113
+ const cli = goke('testcli');
114
+ cli.command('config', 'Save token').action(() => { });
115
+ await addMcpCommands({
116
+ cli,
117
+ argv: ['config', '--token', 'x'],
118
+ getMcpTransport: () => {
119
+ transportCalls += 1;
120
+ return null;
121
+ },
122
+ loadCache: () => undefined,
123
+ saveCache: () => { },
124
+ });
125
+ expect(transportCalls).toBe(0);
126
+ });
90
127
  });
package/dist/index.d.ts CHANGED
@@ -75,12 +75,15 @@ export interface AddMcpCommandsOptions {
75
75
  getMcpUrl?: () => string | undefined;
76
76
  /**
77
77
  * Returns a transport to connect to the MCP server, or null if not configured.
78
- * If null is returned, no MCP tool commands will be registered.
79
- * @param sessionId - Optional session ID from cache to reuse existing session
80
- *
81
- * @deprecated Use getMcpUrl + oauth instead for simpler setup
78
+ * Use this for stdio servers or any setup `getMcpUrl` cannot express.
79
+ * @param sessionId - Optional session ID from a still-valid cache
82
80
  */
83
81
  getMcpTransport?: (sessionId?: string) => Transport | null | Promise<Transport | null>;
82
+ /**
83
+ * Argv used to decide whether to skip live discovery.
84
+ * Defaults to `process.argv.slice(2)`.
85
+ */
86
+ argv?: string[];
84
87
  /**
85
88
  * Extra headers for MCP HTTP requests (for example `Authorization`).
86
89
  * Used with `getMcpUrl`. Ignored when `getMcpTransport` is set.
@@ -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;;;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"}
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;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAEvF;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAEhB;;;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;AA2JD;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4NlF"}
package/dist/index.js CHANGED
@@ -131,7 +131,16 @@ function isHelpOrMetaArgv(argv) {
131
131
  return true;
132
132
  return argv.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v");
133
133
  }
134
- function createTransportWithAuth(url, sessionId, oauthState, oauth, headers) {
134
+ function matchesRegisteredCommand({ argv, cli }) {
135
+ const parts = argv.filter((arg) => !arg.startsWith("-"));
136
+ return cli.commands.some((cmd) => {
137
+ if (!cmd.name)
138
+ return false;
139
+ const nameParts = cmd.name.split(" ");
140
+ return nameParts.every((part, i) => parts[i] === part);
141
+ });
142
+ }
143
+ function createTransportWithAuth({ url, sessionId, oauthState, oauth, headers, }) {
135
144
  let authProvider;
136
145
  if (oauth && oauthState?.tokens) {
137
146
  authProvider = new FileOAuthProvider({
@@ -163,7 +172,7 @@ function createTransportWithAuth(url, sessionId, oauthState, oauth, headers) {
163
172
  * After successful auth, the operation is automatically retried.
164
173
  */
165
174
  export async function addMcpCommands(options) {
166
- const { cli, commandPrefix = "", clientName = "mcp-cli-client", getMcpUrl, getMcpTransport, getHeaders, oauth, loadCache, saveCache, } = options;
175
+ const { cli, commandPrefix = "", clientName = "mcp-cli-client", getMcpUrl, getMcpTransport, getHeaders, oauth, loadCache, saveCache, argv = process.argv.slice(2), } = options;
167
176
  // Helper to get transport - supports both old and new API
168
177
  const getTransport = async (sessionId) => {
169
178
  // New API: getMcpUrl + oauth
@@ -174,9 +183,15 @@ export async function addMcpCommands(options) {
174
183
  }
175
184
  const url = new URL(mcpUrl);
176
185
  const oauthState = oauth?.load();
177
- return createTransportWithAuth(url, sessionId, oauthState, oauth, getHeaders?.());
186
+ return createTransportWithAuth({
187
+ url,
188
+ sessionId,
189
+ oauthState,
190
+ oauth,
191
+ headers: getHeaders?.(),
192
+ });
178
193
  }
179
- // Legacy API: getMcpTransport
194
+ // Custom / stdio transport
180
195
  if (getMcpTransport) {
181
196
  return getMcpTransport(sessionId);
182
197
  }
@@ -209,13 +224,18 @@ export async function addMcpCommands(options) {
209
224
  // Try to use cached tools first (fast path - no network)
210
225
  const cachedTools = loadCache();
211
226
  const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
212
- const helpOrMeta = isHelpOrMetaArgv(process.argv.slice(2));
227
+ const skipLiveDiscovery = isHelpOrMetaArgv(argv) || matchesRegisteredCommand({ argv, cli });
213
228
  let tools;
214
229
  let cachedSessionId;
215
230
  if (isCacheValid && cachedTools) {
216
231
  tools = cachedTools.tools;
217
232
  cachedSessionId = cachedTools.sessionId;
218
233
  }
234
+ else if (skipLiveDiscovery) {
235
+ if (cachedTools) {
236
+ tools = cachedTools.tools;
237
+ }
238
+ }
219
239
  else {
220
240
  const transport = await getTransport();
221
241
  if (transport) {
@@ -237,7 +257,7 @@ export async function addMcpCommands(options) {
237
257
  cachedSessionId = sessionId;
238
258
  }
239
259
  catch (err) {
240
- const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !helpOrMeta;
260
+ const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !skipLiveDiscovery;
241
261
  if (shouldAuth) {
242
262
  const mcpUrl = getMcpUrl();
243
263
  if (mcpUrl) {
@@ -247,7 +267,7 @@ export async function addMcpCommands(options) {
247
267
  }
248
268
  }
249
269
  }
250
- if (!helpOrMeta) {
270
+ if (!skipLiveDiscovery) {
251
271
  console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
252
272
  }
253
273
  }
@@ -257,7 +277,6 @@ export async function addMcpCommands(options) {
257
277
  }
258
278
  if (!tools && cachedTools) {
259
279
  tools = cachedTools.tools;
260
- cachedSessionId = cachedTools.sessionId;
261
280
  }
262
281
  }
263
282
  if (!tools) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goke/mcp",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "description": "Dynamically generate CLI commands from MCP server tools",
6
6
  "repository": {
@@ -1,4 +1,5 @@
1
1
  // First-run help and stale-cache behavior for addMcpCommands.
2
+ import http from 'node:http'
2
3
  import { describe, expect, it } from 'vitest'
3
4
  import { goke } from 'goke'
4
5
  import { addMcpCommands, type CachedMcpTools } from '../index.js'
@@ -12,42 +13,61 @@ const staleCache = (over: Partial<CachedMcpTools> = {}): CachedMcpTools => ({
12
13
  },
13
14
  ],
14
15
  timestamp: Date.now() - 2 * 60 * 60 * 1000,
16
+ sessionId: 'stale-session',
15
17
  ...over,
16
18
  })
17
19
 
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
20
+ function captureErrors() {
21
+ const errors: string[] = []
22
+ const error = console.error
23
+ console.error = (...args: unknown[]) => {
24
+ errors.push(args.map(String).join(' '))
25
25
  }
26
+ return {
27
+ errors,
28
+ restore() {
29
+ console.error = error
30
+ },
31
+ }
32
+ }
33
+
34
+ function listen401() {
35
+ const server = http.createServer((_req, res) => {
36
+ res.writeHead(401, { 'content-type': 'application/json' })
37
+ res.end(JSON.stringify({ error: 'unauthorized' }))
38
+ })
39
+ return new Promise<{ url: string; close: () => Promise<void> }>((resolve) => {
40
+ server.listen(0, '127.0.0.1', () => {
41
+ const addr = server.address()
42
+ if (!addr || typeof addr === 'string') throw new Error('no port')
43
+ resolve({
44
+ url: `http://127.0.0.1:${addr.port}/mcp`,
45
+ close: () => new Promise((done) => server.close(() => done())),
46
+ })
47
+ })
48
+ })
26
49
  }
27
50
 
28
51
  describe('addMcpCommands first-run help', () => {
29
52
  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
-
53
+ const io = captureErrors()
36
54
  const cli = goke('testcli')
37
55
  cli.command('config', 'Save token').action(() => {})
38
56
 
39
- await withArgv(['--help'], async () => {
57
+ try {
40
58
  await addMcpCommands({
41
59
  cli,
60
+ argv: ['--help'],
42
61
  getMcpTransport: () => null,
43
62
  loadCache: () => undefined,
44
63
  saveCache: () => {},
45
64
  })
46
- })
47
- console.error = error
65
+ } finally {
66
+ io.restore()
67
+ }
48
68
 
49
69
  const help = cli.helpText()
50
- expect(errors.join('\n')).not.toMatch(/Failed to connect/)
70
+ expect(io.errors.join('\n')).not.toMatch(/Failed to connect/)
51
71
  expect(help).toMatch(/config/)
52
72
  expect(help).not.toMatch(/find_bookmarks/)
53
73
  })
@@ -55,32 +75,28 @@ describe('addMcpCommands first-run help', () => {
55
75
  it('registers stale cached tools when live fetch is impossible', async () => {
56
76
  const cli = goke('testcli')
57
77
 
58
- await withArgv(['--help'], async () => {
59
- await addMcpCommands({
60
- cli,
61
- getMcpTransport: () => null,
62
- loadCache: () => staleCache(),
63
- saveCache: () => {},
64
- })
78
+ await addMcpCommands({
79
+ cli,
80
+ argv: ['--help'],
81
+ getMcpTransport: () => null,
82
+ loadCache: () => staleCache(),
83
+ saveCache: () => {},
65
84
  })
66
85
 
67
86
  expect(cli.helpText()).toMatch(/find_bookmarks/)
68
87
  })
69
88
 
70
89
  it('does not start OAuth when --help gets a 401', async () => {
90
+ const server = await listen401()
71
91
  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
-
92
+ const io = captureErrors()
78
93
  const cli = goke('testcli')
79
94
 
80
- await withArgv(['--help'], async () => {
95
+ try {
81
96
  await addMcpCommands({
82
97
  cli,
83
- getMcpUrl: () => 'http://127.0.0.1:1/mcp',
98
+ argv: ['--help'],
99
+ getMcpUrl: () => server.url,
84
100
  oauth: {
85
101
  clientName: 'test',
86
102
  load: () => undefined,
@@ -92,11 +108,32 @@ describe('addMcpCommands first-run help', () => {
92
108
  loadCache: () => undefined,
93
109
  saveCache: () => {},
94
110
  })
95
- })
96
- console.error = error
111
+ } finally {
112
+ io.restore()
113
+ await server.close()
114
+ }
97
115
 
98
116
  expect(authUrls).toEqual([])
99
- expect(errors.join('\n')).not.toMatch(/Authentication required/)
117
+ expect(io.errors.join('\n')).not.toMatch(/Authentication required/)
100
118
  expect(cli.helpText()).toMatch(/Usage/)
101
119
  })
120
+
121
+ it('does not connect for an already registered command', async () => {
122
+ let transportCalls = 0
123
+ const cli = goke('testcli')
124
+ cli.command('config', 'Save token').action(() => {})
125
+
126
+ await addMcpCommands({
127
+ cli,
128
+ argv: ['config', '--token', 'x'],
129
+ getMcpTransport: () => {
130
+ transportCalls += 1
131
+ return null
132
+ },
133
+ loadCache: () => undefined,
134
+ saveCache: () => {},
135
+ })
136
+
137
+ expect(transportCalls).toBe(0)
138
+ })
102
139
  })
package/src/index.ts CHANGED
@@ -90,13 +90,17 @@ export interface AddMcpCommandsOptions {
90
90
 
91
91
  /**
92
92
  * Returns a transport to connect to the MCP server, or null if not configured.
93
- * If null is returned, no MCP tool commands will be registered.
94
- * @param sessionId - Optional session ID from cache to reuse existing session
95
- *
96
- * @deprecated Use getMcpUrl + oauth instead for simpler setup
93
+ * Use this for stdio servers or any setup `getMcpUrl` cannot express.
94
+ * @param sessionId - Optional session ID from a still-valid cache
97
95
  */
98
96
  getMcpTransport?: (sessionId?: string) => Transport | null | Promise<Transport | null>;
99
97
 
98
+ /**
99
+ * Argv used to decide whether to skip live discovery.
100
+ * Defaults to `process.argv.slice(2)`.
101
+ */
102
+ argv?: string[];
103
+
100
104
  /**
101
105
  * Extra headers for MCP HTTP requests (for example `Authorization`).
102
106
  * Used with `getMcpUrl`. Ignored when `getMcpTransport` is set.
@@ -236,13 +240,28 @@ function isHelpOrMetaArgv(argv: string[]) {
236
240
  return argv.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v");
237
241
  }
238
242
 
239
- function createTransportWithAuth(
240
- url: URL,
241
- sessionId: string | undefined,
242
- oauthState: McpOAuthState | undefined,
243
- oauth: McpOAuthConfig | undefined,
244
- headers?: Record<string, string>,
245
- ): StreamableHTTPClientTransport {
243
+ function matchesRegisteredCommand({ argv, cli }: { argv: string[]; cli: Goke }) {
244
+ const parts = argv.filter((arg) => !arg.startsWith("-"));
245
+ return cli.commands.some((cmd) => {
246
+ if (!cmd.name) return false;
247
+ const nameParts = cmd.name.split(" ");
248
+ return nameParts.every((part, i) => parts[i] === part);
249
+ });
250
+ }
251
+
252
+ function createTransportWithAuth({
253
+ url,
254
+ sessionId,
255
+ oauthState,
256
+ oauth,
257
+ headers,
258
+ }: {
259
+ url: URL
260
+ sessionId?: string
261
+ oauthState?: McpOAuthState
262
+ oauth?: McpOAuthConfig
263
+ headers?: Record<string, string>
264
+ }): StreamableHTTPClientTransport {
246
265
  let authProvider: FileOAuthProvider | undefined;
247
266
 
248
267
  if (oauth && oauthState?.tokens) {
@@ -289,6 +308,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
289
308
  oauth,
290
309
  loadCache,
291
310
  saveCache,
311
+ argv = process.argv.slice(2),
292
312
  } = options;
293
313
 
294
314
  // Helper to get transport - supports both old and new API
@@ -303,10 +323,16 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
303
323
  const url = new URL(mcpUrl);
304
324
  const oauthState = oauth?.load();
305
325
 
306
- return createTransportWithAuth(url, sessionId, oauthState, oauth, getHeaders?.());
326
+ return createTransportWithAuth({
327
+ url,
328
+ sessionId,
329
+ oauthState,
330
+ oauth,
331
+ headers: getHeaders?.(),
332
+ });
307
333
  }
308
334
 
309
- // Legacy API: getMcpTransport
335
+ // Custom / stdio transport
310
336
  if (getMcpTransport) {
311
337
  return getMcpTransport(sessionId);
312
338
  }
@@ -346,7 +372,8 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
346
372
  // Try to use cached tools first (fast path - no network)
347
373
  const cachedTools = loadCache();
348
374
  const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
349
- const helpOrMeta = isHelpOrMetaArgv(process.argv.slice(2));
375
+ const skipLiveDiscovery =
376
+ isHelpOrMetaArgv(argv) || matchesRegisteredCommand({ argv, cli });
350
377
 
351
378
  let tools: CachedMcpTools["tools"] | undefined;
352
379
  let cachedSessionId: string | undefined;
@@ -354,6 +381,10 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
354
381
  if (isCacheValid && cachedTools) {
355
382
  tools = cachedTools.tools;
356
383
  cachedSessionId = cachedTools.sessionId;
384
+ } else if (skipLiveDiscovery) {
385
+ if (cachedTools) {
386
+ tools = cachedTools.tools;
387
+ }
357
388
  } else {
358
389
  const transport = await getTransport();
359
390
  if (transport) {
@@ -376,7 +407,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
376
407
  });
377
408
  cachedSessionId = sessionId;
378
409
  } catch (err) {
379
- const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !helpOrMeta;
410
+ const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !skipLiveDiscovery;
380
411
  if (shouldAuth) {
381
412
  const mcpUrl = getMcpUrl();
382
413
  if (mcpUrl) {
@@ -386,7 +417,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
386
417
  }
387
418
  }
388
419
  }
389
- if (!helpOrMeta) {
420
+ if (!skipLiveDiscovery) {
390
421
  console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
391
422
  }
392
423
  } finally {
@@ -396,7 +427,6 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
396
427
 
397
428
  if (!tools && cachedTools) {
398
429
  tools = cachedTools.tools;
399
- cachedSessionId = cachedTools.sessionId;
400
430
  }
401
431
  }
402
432