@goke/mcp 0.0.11 → 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 +26 -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 +127 -0
- package/dist/auth.d.ts +2 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -4
- package/dist/index.d.ts +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -40
- package/package.json +2 -2
- package/src/__test__/add-mcp-commands-help.test.ts +139 -0
- package/src/auth.ts +2 -4
- package/src/index.ts +105 -53
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
|
+
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
|
+
|
|
60
|
+
For an HTTP Bearer token, pass `getHeaders`:
|
|
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,10 @@ 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
|
+
| `getMcpTransport` | `(sessionId?) => Transport \| null` | — | Custom transport. Use for stdio or anything `getMcpUrl` cannot express |
|
|
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 |
|
|
471
492
|
| `commandPrefix` | `string` | `''` | Prefix for commands (e.g. `'mcp'` makes `mcp notion-search`) |
|
|
472
493
|
| `clientName` | `string` | `'mcp-cli-client'` | Name sent to the MCP server during connection |
|
|
473
494
|
| `oauth` | `McpOAuthConfig` | — | OAuth config for servers that require authentication |
|
|
@@ -537,6 +558,8 @@ Tools and the MCP session ID are cached for **1 hour** to avoid connecting on ev
|
|
|
537
558
|
|
|
538
559
|
When the cache expires or a tool call fails, the cache is cleared and tools are re-fetched on the next run.
|
|
539
560
|
|
|
561
|
+
If a live `tools/list` cannot run (no transport, 401 during `--help`), `addMcpCommands` still registers tools from an expired cache so help stays useful.
|
|
562
|
+
|
|
540
563
|
## License
|
|
541
564
|
|
|
542
565
|
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,127 @@
|
|
|
1
|
+
// First-run help and stale-cache behavior for addMcpCommands.
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { goke } from 'goke';
|
|
5
|
+
import { addMcpCommands } from '../index.js';
|
|
6
|
+
const staleCache = (over = {}) => ({
|
|
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
|
+
sessionId: 'stale-session',
|
|
16
|
+
...over,
|
|
17
|
+
});
|
|
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
|
+
});
|
|
47
|
+
}
|
|
48
|
+
describe('addMcpCommands first-run help', () => {
|
|
49
|
+
it('lets --help run when there is no token and no cache', async () => {
|
|
50
|
+
const io = captureErrors();
|
|
51
|
+
const cli = goke('testcli');
|
|
52
|
+
cli.command('config', 'Save token').action(() => { });
|
|
53
|
+
try {
|
|
54
|
+
await addMcpCommands({
|
|
55
|
+
cli,
|
|
56
|
+
argv: ['--help'],
|
|
57
|
+
getMcpTransport: () => null,
|
|
58
|
+
loadCache: () => undefined,
|
|
59
|
+
saveCache: () => { },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
io.restore();
|
|
64
|
+
}
|
|
65
|
+
const help = cli.helpText();
|
|
66
|
+
expect(io.errors.join('\n')).not.toMatch(/Failed to connect/);
|
|
67
|
+
expect(help).toMatch(/config/);
|
|
68
|
+
expect(help).not.toMatch(/find_bookmarks/);
|
|
69
|
+
});
|
|
70
|
+
it('registers stale cached tools when live fetch is impossible', async () => {
|
|
71
|
+
const cli = goke('testcli');
|
|
72
|
+
await addMcpCommands({
|
|
73
|
+
cli,
|
|
74
|
+
argv: ['--help'],
|
|
75
|
+
getMcpTransport: () => null,
|
|
76
|
+
loadCache: () => staleCache(),
|
|
77
|
+
saveCache: () => { },
|
|
78
|
+
});
|
|
79
|
+
expect(cli.helpText()).toMatch(/find_bookmarks/);
|
|
80
|
+
});
|
|
81
|
+
it('does not start OAuth when --help gets a 401', async () => {
|
|
82
|
+
const server = await listen401();
|
|
83
|
+
const authUrls = [];
|
|
84
|
+
const io = captureErrors();
|
|
85
|
+
const cli = goke('testcli');
|
|
86
|
+
try {
|
|
87
|
+
await addMcpCommands({
|
|
88
|
+
cli,
|
|
89
|
+
argv: ['--help'],
|
|
90
|
+
getMcpUrl: () => server.url,
|
|
91
|
+
oauth: {
|
|
92
|
+
clientName: 'test',
|
|
93
|
+
load: () => undefined,
|
|
94
|
+
save: () => { },
|
|
95
|
+
onAuthUrl: (url) => {
|
|
96
|
+
authUrls.push(url);
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
loadCache: () => undefined,
|
|
100
|
+
saveCache: () => { },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
io.restore();
|
|
105
|
+
await server.close();
|
|
106
|
+
}
|
|
107
|
+
expect(authUrls).toEqual([]);
|
|
108
|
+
expect(io.errors.join('\n')).not.toMatch(/Authentication required/);
|
|
109
|
+
expect(cli.helpText()).toMatch(/Usage/);
|
|
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
|
+
});
|
|
127
|
+
});
|
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
|
-
*
|
|
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
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
48
|
+
export type { McpOAuthConfig, McpOAuthState } from "./types.js";
|
|
50
49
|
export interface CachedMcpTools {
|
|
51
50
|
tools: Array<{
|
|
52
51
|
name: string;
|
|
@@ -76,12 +75,20 @@ export interface AddMcpCommandsOptions {
|
|
|
76
75
|
getMcpUrl?: () => string | undefined;
|
|
77
76
|
/**
|
|
78
77
|
* Returns a transport to connect to the MCP server, or null if not configured.
|
|
79
|
-
*
|
|
80
|
-
* @param sessionId - Optional session ID from
|
|
81
|
-
*
|
|
82
|
-
* @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
|
|
83
80
|
*/
|
|
84
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[];
|
|
87
|
+
/**
|
|
88
|
+
* Extra headers for MCP HTTP requests (for example `Authorization`).
|
|
89
|
+
* Used with `getMcpUrl`. Ignored when `getMcpTransport` is set.
|
|
90
|
+
*/
|
|
91
|
+
getHeaders?: () => Record<string, string> | undefined;
|
|
85
92
|
/**
|
|
86
93
|
* OAuth configuration. When provided, enables automatic OAuth authentication.
|
|
87
94
|
*
|
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,
|
|
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
|
@@ -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,23 @@ function outputResult(result) {
|
|
|
125
124
|
/**
|
|
126
125
|
* Create a transport with optional OAuth authentication
|
|
127
126
|
*/
|
|
128
|
-
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 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, }) {
|
|
129
144
|
let authProvider;
|
|
130
145
|
if (oauth && oauthState?.tokens) {
|
|
131
146
|
authProvider = new FileOAuthProvider({
|
|
@@ -140,9 +155,11 @@ function createTransportWithAuth(url, sessionId, oauthState, oauth) {
|
|
|
140
155
|
},
|
|
141
156
|
});
|
|
142
157
|
}
|
|
158
|
+
const hasHeaders = headers && Object.keys(headers).length > 0;
|
|
143
159
|
return new StreamableHTTPClientTransport(url, {
|
|
144
160
|
sessionId,
|
|
145
161
|
authProvider,
|
|
162
|
+
requestInit: hasHeaders ? { headers } : undefined,
|
|
146
163
|
});
|
|
147
164
|
}
|
|
148
165
|
/**
|
|
@@ -155,7 +172,7 @@ function createTransportWithAuth(url, sessionId, oauthState, oauth) {
|
|
|
155
172
|
* After successful auth, the operation is automatically retried.
|
|
156
173
|
*/
|
|
157
174
|
export async function addMcpCommands(options) {
|
|
158
|
-
const { cli, commandPrefix = "", clientName = "mcp-cli-client", getMcpUrl, getMcpTransport, oauth, loadCache, saveCache, } = options;
|
|
175
|
+
const { cli, commandPrefix = "", clientName = "mcp-cli-client", getMcpUrl, getMcpTransport, getHeaders, oauth, loadCache, saveCache, argv = process.argv.slice(2), } = options;
|
|
159
176
|
// Helper to get transport - supports both old and new API
|
|
160
177
|
const getTransport = async (sessionId) => {
|
|
161
178
|
// New API: getMcpUrl + oauth
|
|
@@ -166,9 +183,15 @@ export async function addMcpCommands(options) {
|
|
|
166
183
|
}
|
|
167
184
|
const url = new URL(mcpUrl);
|
|
168
185
|
const oauthState = oauth?.load();
|
|
169
|
-
return createTransportWithAuth(
|
|
186
|
+
return createTransportWithAuth({
|
|
187
|
+
url,
|
|
188
|
+
sessionId,
|
|
189
|
+
oauthState,
|
|
190
|
+
oauth,
|
|
191
|
+
headers: getHeaders?.(),
|
|
192
|
+
});
|
|
170
193
|
}
|
|
171
|
-
//
|
|
194
|
+
// Custom / stdio transport
|
|
172
195
|
if (getMcpTransport) {
|
|
173
196
|
return getMcpTransport(sessionId);
|
|
174
197
|
}
|
|
@@ -201,54 +224,64 @@ export async function addMcpCommands(options) {
|
|
|
201
224
|
// Try to use cached tools first (fast path - no network)
|
|
202
225
|
const cachedTools = loadCache();
|
|
203
226
|
const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
|
|
227
|
+
const skipLiveDiscovery = isHelpOrMetaArgv(argv) || matchesRegisteredCommand({ argv, cli });
|
|
204
228
|
let tools;
|
|
205
229
|
let cachedSessionId;
|
|
206
|
-
if (isCacheValid) {
|
|
230
|
+
if (isCacheValid && cachedTools) {
|
|
207
231
|
tools = cachedTools.tools;
|
|
208
232
|
cachedSessionId = cachedTools.sessionId;
|
|
209
233
|
}
|
|
234
|
+
else if (skipLiveDiscovery) {
|
|
235
|
+
if (cachedTools) {
|
|
236
|
+
tools = cachedTools.tools;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
210
239
|
else {
|
|
211
|
-
// Cache invalid/missing - connect to fetch tools
|
|
212
240
|
const transport = await getTransport();
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
sessionId
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Retry after auth
|
|
241
|
-
return addMcpCommands(options);
|
|
241
|
+
if (transport) {
|
|
242
|
+
const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
|
|
243
|
+
try {
|
|
244
|
+
await client.connect(transport);
|
|
245
|
+
const result = await client.listTools();
|
|
246
|
+
tools = result.tools;
|
|
247
|
+
const sessionId = transport.sessionId;
|
|
248
|
+
saveCache({
|
|
249
|
+
tools: tools.map((t) => ({
|
|
250
|
+
name: t.name,
|
|
251
|
+
description: t.description,
|
|
252
|
+
inputSchema: t.inputSchema,
|
|
253
|
+
})),
|
|
254
|
+
timestamp: Date.now(),
|
|
255
|
+
sessionId,
|
|
256
|
+
});
|
|
257
|
+
cachedSessionId = sessionId;
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !skipLiveDiscovery;
|
|
261
|
+
if (shouldAuth) {
|
|
262
|
+
const mcpUrl = getMcpUrl();
|
|
263
|
+
if (mcpUrl) {
|
|
264
|
+
const authSuccess = await handleAuthRequired(mcpUrl);
|
|
265
|
+
if (authSuccess) {
|
|
266
|
+
return addMcpCommands(options);
|
|
267
|
+
}
|
|
242
268
|
}
|
|
243
269
|
}
|
|
270
|
+
if (!skipLiveDiscovery) {
|
|
271
|
+
console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
await client.close();
|
|
244
276
|
}
|
|
245
|
-
console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
|
|
246
|
-
return;
|
|
247
277
|
}
|
|
248
|
-
|
|
249
|
-
|
|
278
|
+
if (!tools && cachedTools) {
|
|
279
|
+
tools = cachedTools.tools;
|
|
250
280
|
}
|
|
251
281
|
}
|
|
282
|
+
if (!tools) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
252
285
|
// Register CLI commands for each tool
|
|
253
286
|
for (const tool of tools) {
|
|
254
287
|
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.13",
|
|
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,139 @@
|
|
|
1
|
+
// First-run help and stale-cache behavior for addMcpCommands.
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { goke } from 'goke'
|
|
5
|
+
import { addMcpCommands, type CachedMcpTools } from '../index.js'
|
|
6
|
+
|
|
7
|
+
const staleCache = (over: Partial<CachedMcpTools> = {}): CachedMcpTools => ({
|
|
8
|
+
tools: [
|
|
9
|
+
{
|
|
10
|
+
name: 'find_bookmarks',
|
|
11
|
+
description: 'Find bookmarks',
|
|
12
|
+
inputSchema: { type: 'object', properties: {} },
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
timestamp: Date.now() - 2 * 60 * 60 * 1000,
|
|
16
|
+
sessionId: 'stale-session',
|
|
17
|
+
...over,
|
|
18
|
+
})
|
|
19
|
+
|
|
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
|
+
}
|
|
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
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('addMcpCommands first-run help', () => {
|
|
52
|
+
it('lets --help run when there is no token and no cache', async () => {
|
|
53
|
+
const io = captureErrors()
|
|
54
|
+
const cli = goke('testcli')
|
|
55
|
+
cli.command('config', 'Save token').action(() => {})
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await addMcpCommands({
|
|
59
|
+
cli,
|
|
60
|
+
argv: ['--help'],
|
|
61
|
+
getMcpTransport: () => null,
|
|
62
|
+
loadCache: () => undefined,
|
|
63
|
+
saveCache: () => {},
|
|
64
|
+
})
|
|
65
|
+
} finally {
|
|
66
|
+
io.restore()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const help = cli.helpText()
|
|
70
|
+
expect(io.errors.join('\n')).not.toMatch(/Failed to connect/)
|
|
71
|
+
expect(help).toMatch(/config/)
|
|
72
|
+
expect(help).not.toMatch(/find_bookmarks/)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('registers stale cached tools when live fetch is impossible', async () => {
|
|
76
|
+
const cli = goke('testcli')
|
|
77
|
+
|
|
78
|
+
await addMcpCommands({
|
|
79
|
+
cli,
|
|
80
|
+
argv: ['--help'],
|
|
81
|
+
getMcpTransport: () => null,
|
|
82
|
+
loadCache: () => staleCache(),
|
|
83
|
+
saveCache: () => {},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(cli.helpText()).toMatch(/find_bookmarks/)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('does not start OAuth when --help gets a 401', async () => {
|
|
90
|
+
const server = await listen401()
|
|
91
|
+
const authUrls: string[] = []
|
|
92
|
+
const io = captureErrors()
|
|
93
|
+
const cli = goke('testcli')
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await addMcpCommands({
|
|
97
|
+
cli,
|
|
98
|
+
argv: ['--help'],
|
|
99
|
+
getMcpUrl: () => server.url,
|
|
100
|
+
oauth: {
|
|
101
|
+
clientName: 'test',
|
|
102
|
+
load: () => undefined,
|
|
103
|
+
save: () => {},
|
|
104
|
+
onAuthUrl: (url) => {
|
|
105
|
+
authUrls.push(url)
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
loadCache: () => undefined,
|
|
109
|
+
saveCache: () => {},
|
|
110
|
+
})
|
|
111
|
+
} finally {
|
|
112
|
+
io.restore()
|
|
113
|
+
await server.close()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
expect(authUrls).toEqual([])
|
|
117
|
+
expect(io.errors.join('\n')).not.toMatch(/Authentication required/)
|
|
118
|
+
expect(cli.helpText()).toMatch(/Usage/)
|
|
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
|
+
})
|
|
139
|
+
})
|
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
|
-
*
|
|
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
|
|
57
|
+
export type { McpOAuthConfig, McpOAuthState } from "./types.js";
|
|
59
58
|
|
|
60
59
|
export interface CachedMcpTools {
|
|
61
60
|
tools: Array<{
|
|
@@ -91,13 +90,23 @@ export interface AddMcpCommandsOptions {
|
|
|
91
90
|
|
|
92
91
|
/**
|
|
93
92
|
* Returns a transport to connect to the MCP server, or null if not configured.
|
|
94
|
-
*
|
|
95
|
-
* @param sessionId - Optional session ID from
|
|
96
|
-
*
|
|
97
|
-
* @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
|
|
98
95
|
*/
|
|
99
96
|
getMcpTransport?: (sessionId?: string) => Transport | null | Promise<Transport | null>;
|
|
100
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Argv used to decide whether to skip live discovery.
|
|
100
|
+
* Defaults to `process.argv.slice(2)`.
|
|
101
|
+
*/
|
|
102
|
+
argv?: string[];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extra headers for MCP HTTP requests (for example `Authorization`).
|
|
106
|
+
* Used with `getMcpUrl`. Ignored when `getMcpTransport` is set.
|
|
107
|
+
*/
|
|
108
|
+
getHeaders?: () => Record<string, string> | undefined;
|
|
109
|
+
|
|
101
110
|
/**
|
|
102
111
|
* OAuth configuration. When provided, enables automatic OAuth authentication.
|
|
103
112
|
*
|
|
@@ -225,12 +234,34 @@ function outputResult(result: {
|
|
|
225
234
|
/**
|
|
226
235
|
* Create a transport with optional OAuth authentication
|
|
227
236
|
*/
|
|
228
|
-
function
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
237
|
+
function isHelpOrMetaArgv(argv: string[]) {
|
|
238
|
+
if (argv.length === 0) return true;
|
|
239
|
+
if (argv[0] === "completions" || argv.includes("--get-goke-completions")) return true;
|
|
240
|
+
return argv.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v");
|
|
241
|
+
}
|
|
242
|
+
|
|
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 {
|
|
234
265
|
let authProvider: FileOAuthProvider | undefined;
|
|
235
266
|
|
|
236
267
|
if (oauth && oauthState?.tokens) {
|
|
@@ -247,9 +278,11 @@ function createTransportWithAuth(
|
|
|
247
278
|
});
|
|
248
279
|
}
|
|
249
280
|
|
|
281
|
+
const hasHeaders = headers && Object.keys(headers).length > 0;
|
|
250
282
|
return new StreamableHTTPClientTransport(url, {
|
|
251
283
|
sessionId,
|
|
252
284
|
authProvider,
|
|
285
|
+
requestInit: hasHeaders ? { headers } : undefined,
|
|
253
286
|
});
|
|
254
287
|
}
|
|
255
288
|
|
|
@@ -271,9 +304,11 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
|
|
|
271
304
|
clientName = "mcp-cli-client",
|
|
272
305
|
getMcpUrl,
|
|
273
306
|
getMcpTransport,
|
|
307
|
+
getHeaders,
|
|
274
308
|
oauth,
|
|
275
309
|
loadCache,
|
|
276
310
|
saveCache,
|
|
311
|
+
argv = process.argv.slice(2),
|
|
277
312
|
} = options;
|
|
278
313
|
|
|
279
314
|
// Helper to get transport - supports both old and new API
|
|
@@ -288,10 +323,16 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
|
|
|
288
323
|
const url = new URL(mcpUrl);
|
|
289
324
|
const oauthState = oauth?.load();
|
|
290
325
|
|
|
291
|
-
return createTransportWithAuth(
|
|
326
|
+
return createTransportWithAuth({
|
|
327
|
+
url,
|
|
328
|
+
sessionId,
|
|
329
|
+
oauthState,
|
|
330
|
+
oauth,
|
|
331
|
+
headers: getHeaders?.(),
|
|
332
|
+
});
|
|
292
333
|
}
|
|
293
334
|
|
|
294
|
-
//
|
|
335
|
+
// Custom / stdio transport
|
|
295
336
|
if (getMcpTransport) {
|
|
296
337
|
return getMcpTransport(sessionId);
|
|
297
338
|
}
|
|
@@ -331,55 +372,66 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
|
|
|
331
372
|
// Try to use cached tools first (fast path - no network)
|
|
332
373
|
const cachedTools = loadCache();
|
|
333
374
|
const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
|
|
375
|
+
const skipLiveDiscovery =
|
|
376
|
+
isHelpOrMetaArgv(argv) || matchesRegisteredCommand({ argv, cli });
|
|
334
377
|
|
|
335
|
-
let tools: CachedMcpTools["tools"];
|
|
378
|
+
let tools: CachedMcpTools["tools"] | undefined;
|
|
336
379
|
let cachedSessionId: string | undefined;
|
|
337
380
|
|
|
338
|
-
if (isCacheValid) {
|
|
381
|
+
if (isCacheValid && cachedTools) {
|
|
339
382
|
tools = cachedTools.tools;
|
|
340
383
|
cachedSessionId = cachedTools.sessionId;
|
|
384
|
+
} else if (skipLiveDiscovery) {
|
|
385
|
+
if (cachedTools) {
|
|
386
|
+
tools = cachedTools.tools;
|
|
387
|
+
}
|
|
341
388
|
} else {
|
|
342
|
-
// Cache invalid/missing - connect to fetch tools
|
|
343
389
|
const transport = await getTransport();
|
|
344
|
-
if (
|
|
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
|
-
|
|
372
|
-
if (authSuccess) {
|
|
373
|
-
// Retry after auth
|
|
374
|
-
return addMcpCommands(options);
|
|
390
|
+
if (transport) {
|
|
391
|
+
const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
|
|
392
|
+
try {
|
|
393
|
+
await client.connect(transport);
|
|
394
|
+
const result = await client.listTools();
|
|
395
|
+
tools = result.tools;
|
|
396
|
+
|
|
397
|
+
const sessionId = (transport as { sessionId?: string }).sessionId;
|
|
398
|
+
|
|
399
|
+
saveCache({
|
|
400
|
+
tools: tools.map((t) => ({
|
|
401
|
+
name: t.name,
|
|
402
|
+
description: t.description,
|
|
403
|
+
inputSchema: t.inputSchema,
|
|
404
|
+
})),
|
|
405
|
+
timestamp: Date.now(),
|
|
406
|
+
sessionId,
|
|
407
|
+
});
|
|
408
|
+
cachedSessionId = sessionId;
|
|
409
|
+
} catch (err) {
|
|
410
|
+
const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !skipLiveDiscovery;
|
|
411
|
+
if (shouldAuth) {
|
|
412
|
+
const mcpUrl = getMcpUrl();
|
|
413
|
+
if (mcpUrl) {
|
|
414
|
+
const authSuccess = await handleAuthRequired(mcpUrl);
|
|
415
|
+
if (authSuccess) {
|
|
416
|
+
return addMcpCommands(options);
|
|
417
|
+
}
|
|
375
418
|
}
|
|
376
419
|
}
|
|
420
|
+
if (!skipLiveDiscovery) {
|
|
421
|
+
console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
|
|
422
|
+
}
|
|
423
|
+
} finally {
|
|
424
|
+
await client.close();
|
|
377
425
|
}
|
|
378
|
-
console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
|
|
379
|
-
return;
|
|
380
|
-
} finally {
|
|
381
|
-
await client.close();
|
|
382
426
|
}
|
|
427
|
+
|
|
428
|
+
if (!tools && cachedTools) {
|
|
429
|
+
tools = cachedTools.tools;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!tools) {
|
|
434
|
+
return;
|
|
383
435
|
}
|
|
384
436
|
|
|
385
437
|
// Register CLI commands for each tool
|