@goke/mcp 0.0.11 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -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/auth.d.ts +2 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -4
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -39
- package/package.json +2 -2
- package/src/__test__/add-mcp-commands-help.test.ts +102 -0
- package/src/auth.ts +2 -4
- package/src/index.ts +64 -42
package/README.md
CHANGED
|
@@ -25,9 +25,9 @@ MCP server Your CLI
|
|
|
25
25
|
|
|
26
26
|
1. **Discover** — calls `tools/list` on the MCP server to get every tool + its JSON Schema
|
|
27
27
|
2. **Register** — creates a CLI command per tool with `--options` derived from the schema
|
|
28
|
-
3. **Cache** — tools and session ID are cached for 1 hour (no network on subsequent runs)
|
|
28
|
+
3. **Cache** — tools and session ID are cached for 1 hour (no network on subsequent runs). Expired cache is still used when a live fetch is impossible (no token, 401 on `--help`)
|
|
29
29
|
4. **Execute** — on invocation, connects to the server and calls the tool with coerced arguments
|
|
30
|
-
5. **OAuth** — if the server returns 401, automatically opens the browser for OAuth, then retries
|
|
30
|
+
5. **OAuth** — if the server returns 401, automatically opens the browser for OAuth, then retries. `--help`, `--version`, `completions`, and no-args never start OAuth
|
|
31
31
|
|
|
32
32
|
## Quick start
|
|
33
33
|
|
|
@@ -55,6 +55,24 @@ cli.completions()
|
|
|
55
55
|
cli.parse()
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
Always pass `getMcpUrl`. Do not return `undefined` just because the user has no token. `--help` and no-args must still run so the CLI can show `config` / login instructions.
|
|
59
|
+
|
|
60
|
+
For a Bearer token, pass `getHeaders` instead of a custom transport:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
await addMcpCommands({
|
|
64
|
+
cli,
|
|
65
|
+
getMcpUrl: () => 'https://api.example.com/mcp',
|
|
66
|
+
getHeaders: () => {
|
|
67
|
+
const token = process.env.EXAMPLE_TOKEN || loadConfig().token
|
|
68
|
+
if (!token) return
|
|
69
|
+
return { Authorization: `Bearer ${token}` }
|
|
70
|
+
},
|
|
71
|
+
loadCache: () => loadConfig().cache,
|
|
72
|
+
saveCache: (cache) => saveConfig({ cache }),
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
58
76
|
That's it. Every tool the MCP server exposes becomes a CLI command:
|
|
59
77
|
|
|
60
78
|
```bash
|
|
@@ -467,7 +485,8 @@ Registers MCP tool commands on a goke CLI instance.
|
|
|
467
485
|
| Option | Type | Default | Description |
|
|
468
486
|
|--------|------|---------|-------------|
|
|
469
487
|
| `cli` | `Goke` | **required** | The goke CLI instance to add commands to |
|
|
470
|
-
| `getMcpUrl` | `() => string \| undefined` | — | Returns the MCP server URL |
|
|
488
|
+
| `getMcpUrl` | `() => string \| undefined` | — | Returns the MCP server URL. Return the URL even when the user is not logged in so `--help` still works |
|
|
489
|
+
| `getHeaders` | `() => Record<string, string> \| undefined` | — | Extra HTTP headers (for example `Authorization`). Used with `getMcpUrl` |
|
|
471
490
|
| `commandPrefix` | `string` | `''` | Prefix for commands (e.g. `'mcp'` makes `mcp notion-search`) |
|
|
472
491
|
| `clientName` | `string` | `'mcp-cli-client'` | Name sent to the MCP server during connection |
|
|
473
492
|
| `oauth` | `McpOAuthConfig` | — | OAuth config for servers that require authentication |
|
|
@@ -537,6 +556,8 @@ Tools and the MCP session ID are cached for **1 hour** to avoid connecting on ev
|
|
|
537
556
|
|
|
538
557
|
When the cache expires or a tool call fails, the cache is cleared and tools are re-fetched on the next run.
|
|
539
558
|
|
|
559
|
+
If a live `tools/list` cannot run (no transport, 401 during `--help`), `addMcpCommands` still registers tools from an expired cache so help stays useful.
|
|
560
|
+
|
|
540
561
|
## License
|
|
541
562
|
|
|
542
563
|
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-mcp-commands-help.test.d.ts","sourceRoot":"","sources":["../../src/__test__/add-mcp-commands-help.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// First-run help and stale-cache behavior for addMcpCommands.
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { goke } from 'goke';
|
|
4
|
+
import { addMcpCommands } from '../index.js';
|
|
5
|
+
const staleCache = (over = {}) => ({
|
|
6
|
+
tools: [
|
|
7
|
+
{
|
|
8
|
+
name: 'find_bookmarks',
|
|
9
|
+
description: 'Find bookmarks',
|
|
10
|
+
inputSchema: { type: 'object', properties: {} },
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
timestamp: Date.now() - 2 * 60 * 60 * 1000,
|
|
14
|
+
...over,
|
|
15
|
+
});
|
|
16
|
+
async function withArgv(argv, fn) {
|
|
17
|
+
const previous = process.argv;
|
|
18
|
+
process.argv = ['node', 'testcli', ...argv];
|
|
19
|
+
try {
|
|
20
|
+
return await fn();
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
process.argv = previous;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
describe('addMcpCommands first-run help', () => {
|
|
27
|
+
it('lets --help run when there is no token and no cache', async () => {
|
|
28
|
+
const errors = [];
|
|
29
|
+
const error = console.error;
|
|
30
|
+
console.error = (...args) => {
|
|
31
|
+
errors.push(args.map(String).join(' '));
|
|
32
|
+
};
|
|
33
|
+
const cli = goke('testcli');
|
|
34
|
+
cli.command('config', 'Save token').action(() => { });
|
|
35
|
+
await withArgv(['--help'], async () => {
|
|
36
|
+
await addMcpCommands({
|
|
37
|
+
cli,
|
|
38
|
+
getMcpTransport: () => null,
|
|
39
|
+
loadCache: () => undefined,
|
|
40
|
+
saveCache: () => { },
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
console.error = error;
|
|
44
|
+
const help = cli.helpText();
|
|
45
|
+
expect(errors.join('\n')).not.toMatch(/Failed to connect/);
|
|
46
|
+
expect(help).toMatch(/config/);
|
|
47
|
+
expect(help).not.toMatch(/find_bookmarks/);
|
|
48
|
+
});
|
|
49
|
+
it('registers stale cached tools when live fetch is impossible', async () => {
|
|
50
|
+
const cli = goke('testcli');
|
|
51
|
+
await withArgv(['--help'], async () => {
|
|
52
|
+
await addMcpCommands({
|
|
53
|
+
cli,
|
|
54
|
+
getMcpTransport: () => null,
|
|
55
|
+
loadCache: () => staleCache(),
|
|
56
|
+
saveCache: () => { },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
expect(cli.helpText()).toMatch(/find_bookmarks/);
|
|
60
|
+
});
|
|
61
|
+
it('does not start OAuth when --help gets a 401', async () => {
|
|
62
|
+
const authUrls = [];
|
|
63
|
+
const errors = [];
|
|
64
|
+
const error = console.error;
|
|
65
|
+
console.error = (...args) => {
|
|
66
|
+
errors.push(args.map(String).join(' '));
|
|
67
|
+
};
|
|
68
|
+
const cli = goke('testcli');
|
|
69
|
+
await withArgv(['--help'], async () => {
|
|
70
|
+
await addMcpCommands({
|
|
71
|
+
cli,
|
|
72
|
+
getMcpUrl: () => 'http://127.0.0.1:1/mcp',
|
|
73
|
+
oauth: {
|
|
74
|
+
clientName: 'test',
|
|
75
|
+
load: () => undefined,
|
|
76
|
+
save: () => { },
|
|
77
|
+
onAuthUrl: (url) => {
|
|
78
|
+
authUrls.push(url);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
loadCache: () => undefined,
|
|
82
|
+
saveCache: () => { },
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
console.error = error;
|
|
86
|
+
expect(authUrls).toEqual([]);
|
|
87
|
+
expect(errors.join('\n')).not.toMatch(/Authentication required/);
|
|
88
|
+
expect(cli.helpText()).toMatch(/Usage/);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { OAuthFlowResult, StartOAuthFlowOptions } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Start the OAuth flow for an MCP server.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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;
|
|
@@ -82,6 +81,11 @@ export interface AddMcpCommandsOptions {
|
|
|
82
81
|
* @deprecated Use getMcpUrl + oauth instead for simpler setup
|
|
83
82
|
*/
|
|
84
83
|
getMcpTransport?: (sessionId?: string) => Transport | null | Promise<Transport | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Extra headers for MCP HTTP requests (for example `Authorization`).
|
|
86
|
+
* Used with `getMcpUrl`. Ignored when `getMcpTransport` is set.
|
|
87
|
+
*/
|
|
88
|
+
getHeaders?: () => Record<string, string> | undefined;
|
|
85
89
|
/**
|
|
86
90
|
* OAuth configuration. When provided, enables automatic OAuth authentication.
|
|
87
91
|
*
|
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;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAEvF;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAEtD;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC;IAEvB;;OAEG;IACH,SAAS,EAAE,MAAM,cAAc,GAAG,SAAS,CAAC;IAE5C;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,KAAK,IAAI,CAAC;CACxD;AA4ID;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiNlF"}
|
package/dist/index.js
CHANGED
|
@@ -45,7 +45,6 @@ import { wrapJsonSchema } from "goke";
|
|
|
45
45
|
import yaml from "js-yaml";
|
|
46
46
|
import { FileOAuthProvider } from "./oauth-provider.js";
|
|
47
47
|
import { startOAuthFlow, isAuthRequiredError } from "./auth.js";
|
|
48
|
-
export { startOAuthFlow } from "./auth.js";
|
|
49
48
|
export { addCliToolsToMcp, createMcpAction } from "./cli-to-mcp.js";
|
|
50
49
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
51
50
|
/**
|
|
@@ -125,7 +124,14 @@ function outputResult(result) {
|
|
|
125
124
|
/**
|
|
126
125
|
* Create a transport with optional OAuth authentication
|
|
127
126
|
*/
|
|
128
|
-
function
|
|
127
|
+
function isHelpOrMetaArgv(argv) {
|
|
128
|
+
if (argv.length === 0)
|
|
129
|
+
return true;
|
|
130
|
+
if (argv[0] === "completions" || argv.includes("--get-goke-completions"))
|
|
131
|
+
return true;
|
|
132
|
+
return argv.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v");
|
|
133
|
+
}
|
|
134
|
+
function createTransportWithAuth(url, sessionId, oauthState, oauth, headers) {
|
|
129
135
|
let authProvider;
|
|
130
136
|
if (oauth && oauthState?.tokens) {
|
|
131
137
|
authProvider = new FileOAuthProvider({
|
|
@@ -140,9 +146,11 @@ function createTransportWithAuth(url, sessionId, oauthState, oauth) {
|
|
|
140
146
|
},
|
|
141
147
|
});
|
|
142
148
|
}
|
|
149
|
+
const hasHeaders = headers && Object.keys(headers).length > 0;
|
|
143
150
|
return new StreamableHTTPClientTransport(url, {
|
|
144
151
|
sessionId,
|
|
145
152
|
authProvider,
|
|
153
|
+
requestInit: hasHeaders ? { headers } : undefined,
|
|
146
154
|
});
|
|
147
155
|
}
|
|
148
156
|
/**
|
|
@@ -155,7 +163,7 @@ function createTransportWithAuth(url, sessionId, oauthState, oauth) {
|
|
|
155
163
|
* After successful auth, the operation is automatically retried.
|
|
156
164
|
*/
|
|
157
165
|
export async function addMcpCommands(options) {
|
|
158
|
-
const { cli, commandPrefix = "", clientName = "mcp-cli-client", getMcpUrl, getMcpTransport, oauth, loadCache, saveCache, } = options;
|
|
166
|
+
const { cli, commandPrefix = "", clientName = "mcp-cli-client", getMcpUrl, getMcpTransport, getHeaders, oauth, loadCache, saveCache, } = options;
|
|
159
167
|
// Helper to get transport - supports both old and new API
|
|
160
168
|
const getTransport = async (sessionId) => {
|
|
161
169
|
// New API: getMcpUrl + oauth
|
|
@@ -166,7 +174,7 @@ export async function addMcpCommands(options) {
|
|
|
166
174
|
}
|
|
167
175
|
const url = new URL(mcpUrl);
|
|
168
176
|
const oauthState = oauth?.load();
|
|
169
|
-
return createTransportWithAuth(url, sessionId, oauthState, oauth);
|
|
177
|
+
return createTransportWithAuth(url, sessionId, oauthState, oauth, getHeaders?.());
|
|
170
178
|
}
|
|
171
179
|
// Legacy API: getMcpTransport
|
|
172
180
|
if (getMcpTransport) {
|
|
@@ -201,54 +209,60 @@ export async function addMcpCommands(options) {
|
|
|
201
209
|
// Try to use cached tools first (fast path - no network)
|
|
202
210
|
const cachedTools = loadCache();
|
|
203
211
|
const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
|
|
212
|
+
const helpOrMeta = isHelpOrMetaArgv(process.argv.slice(2));
|
|
204
213
|
let tools;
|
|
205
214
|
let cachedSessionId;
|
|
206
|
-
if (isCacheValid) {
|
|
215
|
+
if (isCacheValid && cachedTools) {
|
|
207
216
|
tools = cachedTools.tools;
|
|
208
217
|
cachedSessionId = cachedTools.sessionId;
|
|
209
218
|
}
|
|
210
219
|
else {
|
|
211
|
-
// Cache invalid/missing - connect to fetch tools
|
|
212
220
|
const transport = await getTransport();
|
|
213
|
-
if (
|
|
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);
|
|
221
|
+
if (transport) {
|
|
222
|
+
const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
|
|
223
|
+
try {
|
|
224
|
+
await client.connect(transport);
|
|
225
|
+
const result = await client.listTools();
|
|
226
|
+
tools = result.tools;
|
|
227
|
+
const sessionId = transport.sessionId;
|
|
228
|
+
saveCache({
|
|
229
|
+
tools: tools.map((t) => ({
|
|
230
|
+
name: t.name,
|
|
231
|
+
description: t.description,
|
|
232
|
+
inputSchema: t.inputSchema,
|
|
233
|
+
})),
|
|
234
|
+
timestamp: Date.now(),
|
|
235
|
+
sessionId,
|
|
236
|
+
});
|
|
237
|
+
cachedSessionId = sessionId;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !helpOrMeta;
|
|
241
|
+
if (shouldAuth) {
|
|
242
|
+
const mcpUrl = getMcpUrl();
|
|
243
|
+
if (mcpUrl) {
|
|
244
|
+
const authSuccess = await handleAuthRequired(mcpUrl);
|
|
245
|
+
if (authSuccess) {
|
|
246
|
+
return addMcpCommands(options);
|
|
247
|
+
}
|
|
242
248
|
}
|
|
243
249
|
}
|
|
250
|
+
if (!helpOrMeta) {
|
|
251
|
+
console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await client.close();
|
|
244
256
|
}
|
|
245
|
-
console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
|
|
246
|
-
return;
|
|
247
257
|
}
|
|
248
|
-
|
|
249
|
-
|
|
258
|
+
if (!tools && cachedTools) {
|
|
259
|
+
tools = cachedTools.tools;
|
|
260
|
+
cachedSessionId = cachedTools.sessionId;
|
|
250
261
|
}
|
|
251
262
|
}
|
|
263
|
+
if (!tools) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
252
266
|
// Register CLI commands for each tool
|
|
253
267
|
for (const tool of tools) {
|
|
254
268
|
const inputSchema = tool.inputSchema;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goke/mcp",
|
|
3
|
-
"version": "0.0.
|
|
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/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<{
|
|
@@ -98,6 +97,12 @@ export interface AddMcpCommandsOptions {
|
|
|
98
97
|
*/
|
|
99
98
|
getMcpTransport?: (sessionId?: string) => Transport | null | Promise<Transport | null>;
|
|
100
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Extra headers for MCP HTTP requests (for example `Authorization`).
|
|
102
|
+
* Used with `getMcpUrl`. Ignored when `getMcpTransport` is set.
|
|
103
|
+
*/
|
|
104
|
+
getHeaders?: () => Record<string, string> | undefined;
|
|
105
|
+
|
|
101
106
|
/**
|
|
102
107
|
* OAuth configuration. When provided, enables automatic OAuth authentication.
|
|
103
108
|
*
|
|
@@ -225,11 +230,18 @@ function outputResult(result: {
|
|
|
225
230
|
/**
|
|
226
231
|
* Create a transport with optional OAuth authentication
|
|
227
232
|
*/
|
|
233
|
+
function isHelpOrMetaArgv(argv: string[]) {
|
|
234
|
+
if (argv.length === 0) return true;
|
|
235
|
+
if (argv[0] === "completions" || argv.includes("--get-goke-completions")) return true;
|
|
236
|
+
return argv.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v");
|
|
237
|
+
}
|
|
238
|
+
|
|
228
239
|
function createTransportWithAuth(
|
|
229
240
|
url: URL,
|
|
230
241
|
sessionId: string | undefined,
|
|
231
242
|
oauthState: McpOAuthState | undefined,
|
|
232
243
|
oauth: McpOAuthConfig | undefined,
|
|
244
|
+
headers?: Record<string, string>,
|
|
233
245
|
): StreamableHTTPClientTransport {
|
|
234
246
|
let authProvider: FileOAuthProvider | undefined;
|
|
235
247
|
|
|
@@ -247,9 +259,11 @@ function createTransportWithAuth(
|
|
|
247
259
|
});
|
|
248
260
|
}
|
|
249
261
|
|
|
262
|
+
const hasHeaders = headers && Object.keys(headers).length > 0;
|
|
250
263
|
return new StreamableHTTPClientTransport(url, {
|
|
251
264
|
sessionId,
|
|
252
265
|
authProvider,
|
|
266
|
+
requestInit: hasHeaders ? { headers } : undefined,
|
|
253
267
|
});
|
|
254
268
|
}
|
|
255
269
|
|
|
@@ -271,6 +285,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
|
|
|
271
285
|
clientName = "mcp-cli-client",
|
|
272
286
|
getMcpUrl,
|
|
273
287
|
getMcpTransport,
|
|
288
|
+
getHeaders,
|
|
274
289
|
oauth,
|
|
275
290
|
loadCache,
|
|
276
291
|
saveCache,
|
|
@@ -288,7 +303,7 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
|
|
|
288
303
|
const url = new URL(mcpUrl);
|
|
289
304
|
const oauthState = oauth?.load();
|
|
290
305
|
|
|
291
|
-
return createTransportWithAuth(url, sessionId, oauthState, oauth);
|
|
306
|
+
return createTransportWithAuth(url, sessionId, oauthState, oauth, getHeaders?.());
|
|
292
307
|
}
|
|
293
308
|
|
|
294
309
|
// Legacy API: getMcpTransport
|
|
@@ -331,55 +346,62 @@ export async function addMcpCommands(options: AddMcpCommandsOptions): Promise<vo
|
|
|
331
346
|
// Try to use cached tools first (fast path - no network)
|
|
332
347
|
const cachedTools = loadCache();
|
|
333
348
|
const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
|
|
349
|
+
const helpOrMeta = isHelpOrMetaArgv(process.argv.slice(2));
|
|
334
350
|
|
|
335
|
-
let tools: CachedMcpTools["tools"];
|
|
351
|
+
let tools: CachedMcpTools["tools"] | undefined;
|
|
336
352
|
let cachedSessionId: string | undefined;
|
|
337
353
|
|
|
338
|
-
if (isCacheValid) {
|
|
354
|
+
if (isCacheValid && cachedTools) {
|
|
339
355
|
tools = cachedTools.tools;
|
|
340
356
|
cachedSessionId = cachedTools.sessionId;
|
|
341
357
|
} else {
|
|
342
|
-
// Cache invalid/missing - connect to fetch tools
|
|
343
358
|
const transport = await getTransport();
|
|
344
|
-
if (
|
|
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);
|
|
359
|
+
if (transport) {
|
|
360
|
+
const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
|
|
361
|
+
try {
|
|
362
|
+
await client.connect(transport);
|
|
363
|
+
const result = await client.listTools();
|
|
364
|
+
tools = result.tools;
|
|
365
|
+
|
|
366
|
+
const sessionId = (transport as { sessionId?: string }).sessionId;
|
|
367
|
+
|
|
368
|
+
saveCache({
|
|
369
|
+
tools: tools.map((t) => ({
|
|
370
|
+
name: t.name,
|
|
371
|
+
description: t.description,
|
|
372
|
+
inputSchema: t.inputSchema,
|
|
373
|
+
})),
|
|
374
|
+
timestamp: Date.now(),
|
|
375
|
+
sessionId,
|
|
376
|
+
});
|
|
377
|
+
cachedSessionId = sessionId;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const shouldAuth = isAuthRequiredError(err) && oauth && getMcpUrl && !helpOrMeta;
|
|
380
|
+
if (shouldAuth) {
|
|
381
|
+
const mcpUrl = getMcpUrl();
|
|
382
|
+
if (mcpUrl) {
|
|
383
|
+
const authSuccess = await handleAuthRequired(mcpUrl);
|
|
384
|
+
if (authSuccess) {
|
|
385
|
+
return addMcpCommands(options);
|
|
386
|
+
}
|
|
375
387
|
}
|
|
376
388
|
}
|
|
389
|
+
if (!helpOrMeta) {
|
|
390
|
+
console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
|
|
391
|
+
}
|
|
392
|
+
} finally {
|
|
393
|
+
await client.close();
|
|
377
394
|
}
|
|
378
|
-
console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
|
|
379
|
-
return;
|
|
380
|
-
} finally {
|
|
381
|
-
await client.close();
|
|
382
395
|
}
|
|
396
|
+
|
|
397
|
+
if (!tools && cachedTools) {
|
|
398
|
+
tools = cachedTools.tools;
|
|
399
|
+
cachedSessionId = cachedTools.sessionId;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!tools) {
|
|
404
|
+
return;
|
|
383
405
|
}
|
|
384
406
|
|
|
385
407
|
// Register CLI commands for each tool
|