@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 +27 -3
- package/dist/__test__/add-mcp-commands-help.test.d.ts +2 -0
- package/dist/__test__/add-mcp-commands-help.test.d.ts.map +1 -0
- package/dist/__test__/add-mcp-commands-help.test.js +90 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -38
- package/package.json +2 -2
- package/src/__test__/add-mcp-commands-help.test.ts +102 -0
- package/src/index.ts +62 -39
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 @@
|
|
|
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
|
*
|
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;;;;;;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;
|
|
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
|
|
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 (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
sessionId
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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.
|
|
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.
|
|
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 (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|