@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 +4 -2
- package/dist/__test__/add-mcp-commands-help.test.js +72 -35
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -8
- package/package.json +1 -1
- package/src/__test__/add-mcp-commands-help.test.ts +72 -35
- package/src/index.ts +47 -17
package/README.md
CHANGED
|
@@ -55,9 +55,9 @@ cli.completions()
|
|
|
55
55
|
cli.parse()
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
|
|
86
|
+
try {
|
|
70
87
|
await addMcpCommands({
|
|
71
88
|
cli,
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
79
|
-
* @param sessionId - Optional session ID from
|
|
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.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
186
|
+
return createTransportWithAuth({
|
|
187
|
+
url,
|
|
188
|
+
sessionId,
|
|
189
|
+
oauthState,
|
|
190
|
+
oauth,
|
|
191
|
+
headers: getHeaders?.(),
|
|
192
|
+
});
|
|
178
193
|
}
|
|
179
|
-
//
|
|
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
|
|
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 && !
|
|
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 (!
|
|
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,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
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
95
|
+
try {
|
|
81
96
|
await addMcpCommands({
|
|
82
97
|
cli,
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
94
|
-
* @param sessionId - Optional session ID from
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
)
|
|
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(
|
|
326
|
+
return createTransportWithAuth({
|
|
327
|
+
url,
|
|
328
|
+
sessionId,
|
|
329
|
+
oauthState,
|
|
330
|
+
oauth,
|
|
331
|
+
headers: getHeaders?.(),
|
|
332
|
+
});
|
|
307
333
|
}
|
|
308
334
|
|
|
309
|
-
//
|
|
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
|
|
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 && !
|
|
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 (!
|
|
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
|
|