@goke/mcp 0.0.4

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/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ /**
2
+ * # MCP to CLI
3
+ *
4
+ * Dynamically generates CLI commands from MCP (Model Context Protocol) server tools.
5
+ * This module connects to any MCP server, discovers available tools, and creates
6
+ * corresponding CLI commands with proper argument parsing and validation.
7
+ *
8
+ * ## Features
9
+ *
10
+ * - **Auto-discovery**: Fetches all tools from the MCP server and creates CLI commands
11
+ * - **Caching**: Tools are cached for 1 hour to avoid reconnecting on every invocation
12
+ * - **Session reuse**: MCP session IDs are cached to skip initialization handshake
13
+ * - **Type-aware parsing**: Handles string, number, boolean, object, and array arguments
14
+ * - **JSON schema support**: Generates CLI options from tool input schemas
15
+ * - **OAuth support**: Automatic OAuth authentication on 401 errors (lazy auth)
16
+ *
17
+ * ## Example Usage
18
+ *
19
+ * ```ts
20
+ * import { goke } from 'goke'
21
+ * import { addMcpCommands } from '@goke/mcp'
22
+ *
23
+ * const cli = goke('mycli')
24
+ *
25
+ * await addMcpCommands({
26
+ * cli,
27
+ * getMcpUrl: () => 'https://your-mcp-server.com/mcp',
28
+ * oauth: {
29
+ * clientName: 'My CLI',
30
+ * load: () => loadConfig().oauthState,
31
+ * save: (state) => saveConfig({ oauthState: state }),
32
+ * },
33
+ * loadCache: () => loadConfig().cache,
34
+ * saveCache: (cache) => saveConfig({ cache }),
35
+ * })
36
+ *
37
+ * cli.parse()
38
+ * ```
39
+ *
40
+ * @module @goke/mcp
41
+ */
42
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
43
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
44
+ import { wrapJsonSchema } from "goke";
45
+ import yaml from "js-yaml";
46
+ import { FileOAuthProvider } from "./oauth-provider.js";
47
+ import { startOAuthFlow, isAuthRequiredError } from "./auth.js";
48
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
49
+ /**
50
+ * Check if a schema represents a complex type (object/array) for help text display.
51
+ */
52
+ function isComplexJsonSchema(schema) {
53
+ const type = schema.type;
54
+ if (type) {
55
+ const types = Array.isArray(type) ? type : [type];
56
+ if (types.some((t) => t === "object" || t === "array"))
57
+ return true;
58
+ }
59
+ if (schema.properties || schema.additionalProperties)
60
+ return true;
61
+ if (schema.items)
62
+ return true;
63
+ const unionTypes = [...(schema.anyOf || []), ...(schema.oneOf || []), ...(schema.allOf || [])];
64
+ return unionTypes.some((s) => isComplexJsonSchema(s));
65
+ }
66
+ /**
67
+ * Convert JSON schema to compact JSON string for display
68
+ */
69
+ function schemaToString(schema) {
70
+ const compact = { ...schema };
71
+ delete compact.description;
72
+ return JSON.stringify(compact);
73
+ }
74
+ /**
75
+ * Extract tool arguments from CLI options.
76
+ * Goke now handles all type coercion via schemas, so this just picks
77
+ * the relevant keys from the parsed options.
78
+ */
79
+ function extractToolArguments(options, inputSchema) {
80
+ const args = {};
81
+ if (!inputSchema?.properties) {
82
+ return args;
83
+ }
84
+ for (const name of Object.keys(inputSchema.properties)) {
85
+ const value = options[name];
86
+ if (value !== undefined) {
87
+ args[name] = value;
88
+ }
89
+ }
90
+ return args;
91
+ }
92
+ function outputResult(result) {
93
+ for (const block of result.content) {
94
+ if (block.type === "text" && block.text) {
95
+ // Try to parse as JSON and format as YAML for better readability
96
+ try {
97
+ const parsed = JSON.parse(block.text);
98
+ if (parsed) {
99
+ console.log(yaml.dump(parsed, {
100
+ indent: 2,
101
+ lineWidth: 120,
102
+ noRefs: true,
103
+ sortKeys: false,
104
+ }));
105
+ }
106
+ else {
107
+ console.log(block.text);
108
+ }
109
+ }
110
+ catch {
111
+ // Not JSON, output as-is
112
+ console.log(block.text);
113
+ }
114
+ }
115
+ else if (block.type === "image") {
116
+ console.log("[Image content omitted]");
117
+ }
118
+ else {
119
+ console.log(JSON.stringify(block, null, 2));
120
+ }
121
+ }
122
+ }
123
+ /**
124
+ * Create a transport with optional OAuth authentication
125
+ */
126
+ function createTransportWithAuth(url, sessionId, oauthState, oauth) {
127
+ let authProvider;
128
+ if (oauth && oauthState?.tokens) {
129
+ authProvider = new FileOAuthProvider({
130
+ serverUrl: url.toString(),
131
+ redirectUri: "http://localhost/callback", // Placeholder, real one set during auth flow
132
+ clientName: oauth.clientName,
133
+ tokens: oauthState.tokens,
134
+ clientInformation: oauthState.clientInformation,
135
+ codeVerifier: oauthState.codeVerifier,
136
+ onStateUpdated: (newState) => {
137
+ oauth.save(newState);
138
+ },
139
+ });
140
+ }
141
+ return new StreamableHTTPClientTransport(url, {
142
+ sessionId,
143
+ authProvider,
144
+ });
145
+ }
146
+ /**
147
+ * Adds MCP tool commands to a goke CLI instance.
148
+ *
149
+ * Tools are cached for 1 hour to avoid connecting on every CLI invocation.
150
+ * Session ID is also cached to skip MCP initialization handshake.
151
+ *
152
+ * OAuth is lazy - authentication only happens when a 401 error occurs.
153
+ * After successful auth, the operation is automatically retried.
154
+ */
155
+ export async function addMcpCommands(options) {
156
+ const { cli, commandPrefix = "", clientName = "mcp-cli-client", getMcpUrl, getMcpTransport, oauth, loadCache, saveCache, } = options;
157
+ // Helper to get transport - supports both old and new API
158
+ const getTransport = async (sessionId) => {
159
+ // New API: getMcpUrl + oauth
160
+ if (getMcpUrl) {
161
+ const mcpUrl = getMcpUrl();
162
+ if (!mcpUrl) {
163
+ return null;
164
+ }
165
+ const url = new URL(mcpUrl);
166
+ const oauthState = oauth?.load();
167
+ return createTransportWithAuth(url, sessionId, oauthState, oauth);
168
+ }
169
+ // Legacy API: getMcpTransport
170
+ if (getMcpTransport) {
171
+ return getMcpTransport(sessionId);
172
+ }
173
+ return null;
174
+ };
175
+ // Handle auth required - triggers OAuth flow internally
176
+ const handleAuthRequired = async (serverUrl) => {
177
+ if (!oauth) {
178
+ console.error("Authentication required but OAuth not configured.");
179
+ console.error("Add oauth config to addMcpCommands() to enable automatic authentication.");
180
+ return false;
181
+ }
182
+ console.log("\n🔐 Authentication required. Opening browser...\n");
183
+ const result = await startOAuthFlow({
184
+ serverUrl,
185
+ clientName: oauth.clientName,
186
+ existingState: oauth.load(),
187
+ onAuthUrl: oauth.onAuthUrl,
188
+ });
189
+ if (result.success && result.state) {
190
+ oauth.save(result.state);
191
+ oauth.onAuthSuccess?.();
192
+ console.log("✓ Authentication successful! Retrying...\n");
193
+ return true;
194
+ }
195
+ oauth.onAuthError?.(result.error || "Unknown error");
196
+ console.error(`✗ Authentication failed: ${result.error}\n`);
197
+ return false;
198
+ };
199
+ // Try to use cached tools first (fast path - no network)
200
+ const cachedTools = loadCache();
201
+ const isCacheValid = cachedTools && (Date.now() - cachedTools.timestamp) < CACHE_TTL_MS;
202
+ let tools;
203
+ let cachedSessionId;
204
+ if (isCacheValid) {
205
+ tools = cachedTools.tools;
206
+ cachedSessionId = cachedTools.sessionId;
207
+ }
208
+ else {
209
+ // Cache invalid/missing - connect to fetch tools
210
+ const transport = await getTransport();
211
+ if (!transport) {
212
+ return;
213
+ }
214
+ const client = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
215
+ try {
216
+ await client.connect(transport);
217
+ const result = await client.listTools();
218
+ tools = result.tools;
219
+ const sessionId = transport.sessionId;
220
+ saveCache({
221
+ tools: tools.map((t) => ({
222
+ name: t.name,
223
+ description: t.description,
224
+ inputSchema: t.inputSchema,
225
+ })),
226
+ timestamp: Date.now(),
227
+ sessionId,
228
+ });
229
+ cachedSessionId = sessionId;
230
+ }
231
+ catch (err) {
232
+ // Check if auth is required during tool discovery
233
+ if (isAuthRequiredError(err) && oauth && getMcpUrl) {
234
+ const mcpUrl = getMcpUrl();
235
+ if (mcpUrl) {
236
+ const authSuccess = await handleAuthRequired((mcpUrl).toString());
237
+ if (authSuccess) {
238
+ // Retry after auth
239
+ return addMcpCommands(options);
240
+ }
241
+ }
242
+ }
243
+ console.error(`Failed to connect to MCP server: ${err instanceof Error ? err.message : err}`);
244
+ return;
245
+ }
246
+ finally {
247
+ await client.close();
248
+ }
249
+ }
250
+ // Register CLI commands for each tool
251
+ for (const tool of tools) {
252
+ const inputSchema = tool.inputSchema;
253
+ const cmdName = commandPrefix ? `${commandPrefix} ${tool.name}` : tool.name;
254
+ const description = tool.description || `Run MCP tool ${tool.name}`;
255
+ const cmd = cli.command(cmdName, description);
256
+ if (inputSchema?.properties) {
257
+ for (const [propName, propSchema] of Object.entries(inputSchema.properties)) {
258
+ const isRequired = inputSchema.required?.includes(propName) ?? false;
259
+ const schemaType = propSchema.type;
260
+ // Only treat as boolean flag if type is exclusively "boolean".
261
+ // Union types like ["boolean", "string"] should take a value.
262
+ const isBooleanType = schemaType === "boolean";
263
+ const optionStr = isBooleanType ? `--${propName}` : `--${propName} <${propName}>`;
264
+ let optionDesc = propSchema.description || propName;
265
+ if (isRequired) {
266
+ optionDesc += " (required)";
267
+ }
268
+ if (isComplexJsonSchema(propSchema)) {
269
+ optionDesc += ` (JSON: ${schemaToString(propSchema)})`;
270
+ }
271
+ if (isBooleanType && propSchema.default === undefined) {
272
+ // Boolean flags without defaults don't need schema — mri handles them natively.
273
+ cmd.option(optionStr, optionDesc);
274
+ }
275
+ else {
276
+ // Wrap the MCP tool's JSON Schema property into a StandardJSONSchemaV1
277
+ // object so Goke can use it for type coercion (string → typed value).
278
+ // Put the enriched description into the JSON Schema so it's extracted automatically.
279
+ // Boolean flags with defaults also go through this path to preserve the default.
280
+ const enrichedSchema = { ...propSchema, description: optionDesc };
281
+ cmd.option(optionStr, wrapJsonSchema(enrichedSchema));
282
+ }
283
+ }
284
+ }
285
+ cmd.action(async (cliOptions) => {
286
+ // Goke already coerced all values via schemas — just extract the relevant keys
287
+ const parsedArgs = extractToolArguments(cliOptions, inputSchema);
288
+ const executeWithRetry = async (isRetry = false) => {
289
+ const transport = await getTransport(isRetry ? undefined : cachedSessionId);
290
+ if (!transport) {
291
+ console.error("MCP transport not available. Run login command first.");
292
+ process.exit(1);
293
+ }
294
+ const actionClient = new Client({ name: clientName, version: "1.0.0" }, { capabilities: {} });
295
+ try {
296
+ await actionClient.connect(transport);
297
+ const result = await actionClient.callTool({ name: tool.name, arguments: parsedArgs });
298
+ outputResult(result);
299
+ }
300
+ catch (err) {
301
+ // On 401, trigger OAuth and retry (only once)
302
+ if (!isRetry && isAuthRequiredError(err) && oauth && getMcpUrl) {
303
+ const mcpUrl = getMcpUrl();
304
+ if (mcpUrl) {
305
+ const authSuccess = await handleAuthRequired((mcpUrl).toString());
306
+ if (authSuccess) {
307
+ await actionClient.close();
308
+ return executeWithRetry(true);
309
+ }
310
+ }
311
+ }
312
+ // Clear cache on error (might be stale)
313
+ saveCache(undefined);
314
+ console.error(`Error calling ${tool.name}:`, err instanceof Error ? err.message : err);
315
+ process.exit(1);
316
+ }
317
+ finally {
318
+ await actionClient.close();
319
+ }
320
+ };
321
+ await executeWithRetry();
322
+ });
323
+ }
324
+ }
@@ -0,0 +1,14 @@
1
+ import type { CallbackResult, CallbackServerOptions } from "./types.js";
2
+ /**
3
+ * Start a local HTTP server to receive OAuth callbacks.
4
+ * Uses a random available port to avoid conflicts.
5
+ *
6
+ * @returns Object with port, redirectUri, waitForCallback promise, and close function
7
+ */
8
+ export declare function startCallbackServer(options?: CallbackServerOptions): Promise<{
9
+ port: number;
10
+ redirectUri: string;
11
+ waitForCallback: () => Promise<CallbackResult>;
12
+ close: () => void;
13
+ }>;
14
+ //# sourceMappingURL=local-callback-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-callback-server.d.ts","sourceRoot":"","sources":["../src/local-callback-server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AA2FxE;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/C,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC,CAgFD"}
@@ -0,0 +1,162 @@
1
+ import http from "node:http";
2
+ import net from "node:net";
3
+ const DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes
4
+ /**
5
+ * Find a random available port by letting the OS assign one.
6
+ */
7
+ async function findAvailablePort() {
8
+ return new Promise((resolve, reject) => {
9
+ const server = net.createServer();
10
+ server.on("error", reject);
11
+ server.listen(0, () => {
12
+ const address = server.address();
13
+ if (!address || typeof address === "string") {
14
+ server.close();
15
+ reject(new Error("Failed to get port from server"));
16
+ return;
17
+ }
18
+ const port = address.port;
19
+ server.close(() => {
20
+ resolve(port);
21
+ });
22
+ });
23
+ });
24
+ }
25
+ /**
26
+ * Generate HTML response for the callback page
27
+ */
28
+ function generateCallbackHtml(success, message) {
29
+ const color = success ? "#22c55e" : "#ef4444";
30
+ const icon = success ? "✓" : "✗";
31
+ return `<!DOCTYPE html>
32
+ <html lang="en">
33
+ <head>
34
+ <meta charset="UTF-8">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
+ <title>${success ? "Authentication Successful" : "Authentication Failed"}</title>
37
+ <style>
38
+ body {
39
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ min-height: 100vh;
44
+ margin: 0;
45
+ background-color: #f5f5f5;
46
+ }
47
+ .container {
48
+ background: white;
49
+ padding: 3rem;
50
+ border-radius: 12px;
51
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
52
+ text-align: center;
53
+ max-width: 400px;
54
+ }
55
+ .icon {
56
+ font-size: 4rem;
57
+ color: ${color};
58
+ margin-bottom: 1rem;
59
+ }
60
+ h1 {
61
+ color: #1f2937;
62
+ margin-bottom: 0.5rem;
63
+ }
64
+ p {
65
+ color: #6b7280;
66
+ margin-bottom: 1.5rem;
67
+ }
68
+ .hint {
69
+ font-size: 0.875rem;
70
+ color: #9ca3af;
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div class="container">
76
+ <div class="icon">${icon}</div>
77
+ <h1>${success ? "Authentication Successful" : "Authentication Failed"}</h1>
78
+ <p>${message}</p>
79
+ <p class="hint">You can close this window and return to your terminal.</p>
80
+ </div>
81
+ <script>
82
+ // Try to close the window after a short delay
83
+ setTimeout(() => { window.close(); }, 2000);
84
+ </script>
85
+ </body>
86
+ </html>`;
87
+ }
88
+ /**
89
+ * Start a local HTTP server to receive OAuth callbacks.
90
+ * Uses a random available port to avoid conflicts.
91
+ *
92
+ * @returns Object with port, redirectUri, waitForCallback promise, and close function
93
+ */
94
+ export async function startCallbackServer(options = {}) {
95
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
96
+ const port = await findAvailablePort();
97
+ const redirectUri = `http://localhost:${port}/callback`;
98
+ let resolveCallback;
99
+ let rejectCallback;
100
+ let timeoutId;
101
+ const callbackPromise = new Promise((resolve, reject) => {
102
+ resolveCallback = resolve;
103
+ rejectCallback = reject;
104
+ });
105
+ const server = http.createServer((req, res) => {
106
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
107
+ // Only handle the callback path
108
+ if (url.pathname !== "/callback") {
109
+ res.writeHead(404, { "Content-Type": "text/plain" });
110
+ res.end("Not Found");
111
+ return;
112
+ }
113
+ const code = url.searchParams.get("code");
114
+ const state = url.searchParams.get("state");
115
+ const error = url.searchParams.get("error");
116
+ const errorDescription = url.searchParams.get("error_description");
117
+ if (error) {
118
+ const message = errorDescription || error;
119
+ res.writeHead(200, { "Content-Type": "text/html" });
120
+ res.end(generateCallbackHtml(false, message));
121
+ rejectCallback?.(new Error(`OAuth error: ${message}`));
122
+ return;
123
+ }
124
+ if (!code) {
125
+ res.writeHead(400, { "Content-Type": "text/html" });
126
+ res.end(generateCallbackHtml(false, "Missing authorization code"));
127
+ rejectCallback?.(new Error("Missing authorization code"));
128
+ return;
129
+ }
130
+ res.writeHead(200, { "Content-Type": "text/html" });
131
+ res.end(generateCallbackHtml(true, "You have been authenticated successfully."));
132
+ resolveCallback?.({ code, state: state || "" });
133
+ });
134
+ server.listen(port, () => {
135
+ options.onReady?.(redirectUri);
136
+ });
137
+ // Set up timeout
138
+ timeoutId = setTimeout(() => {
139
+ rejectCallback?.(new Error(`OAuth callback timed out after ${timeout / 1000} seconds`));
140
+ server.close();
141
+ }, timeout);
142
+ const close = () => {
143
+ if (timeoutId) {
144
+ clearTimeout(timeoutId);
145
+ }
146
+ server.close();
147
+ };
148
+ const waitForCallback = async () => {
149
+ try {
150
+ return await callbackPromise;
151
+ }
152
+ finally {
153
+ close();
154
+ }
155
+ };
156
+ return {
157
+ port,
158
+ redirectUri,
159
+ waitForCallback,
160
+ close,
161
+ };
162
+ }
@@ -0,0 +1,63 @@
1
+ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import type { OAuthClientInformation, OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
3
+ import type { McpOAuthState } from "./types.js";
4
+ export interface FileOAuthProviderOptions {
5
+ serverUrl: string;
6
+ redirectUri: string;
7
+ clientName: string;
8
+ tokens?: OAuthTokens;
9
+ clientInformation?: OAuthClientInformation;
10
+ codeVerifier?: string;
11
+ /**
12
+ * Called when tokens are updated (initial save or refresh).
13
+ * Use this to persist the new state.
14
+ */
15
+ onStateUpdated?: (state: McpOAuthState) => void;
16
+ }
17
+ /**
18
+ * File-based OAuth provider implementation for CLI usage.
19
+ * Implements the OAuthClientProvider interface from MCP SDK.
20
+ *
21
+ * Unlike server-based implementations that use a database,
22
+ * this stores state in memory and calls onStateUpdated for persistence.
23
+ */
24
+ export declare class FileOAuthProvider implements OAuthClientProvider {
25
+ private _clientInformation;
26
+ private _codeVerifier;
27
+ private _tokens;
28
+ private _redirectStartAuthUrl;
29
+ private readonly serverUrl;
30
+ private readonly redirectUri;
31
+ private readonly clientName;
32
+ private readonly onStateUpdated?;
33
+ constructor(options: FileOAuthProviderOptions);
34
+ get redirectUrl(): string;
35
+ /**
36
+ * The authorization URL to redirect the user to.
37
+ * Set by redirectToAuthorization().
38
+ */
39
+ get redirectStartAuthUrl(): URL | undefined;
40
+ clientInformation(): Promise<OAuthClientInformation | undefined>;
41
+ saveClientInformation(clientInformation: OAuthClientInformationFull): Promise<void>;
42
+ codeVerifier(): Promise<string>;
43
+ saveCodeVerifier(codeVerifier: string): Promise<void>;
44
+ get clientMetadata(): OAuthClientMetadata;
45
+ /**
46
+ * Called by the MCP SDK when the user needs to be redirected to authorize.
47
+ * We store the URL so the CLI can open it in the browser.
48
+ */
49
+ redirectToAuthorization(authorizationUrl: URL): void;
50
+ tokens(): Promise<OAuthTokens | undefined>;
51
+ saveTokens(tokens: OAuthTokens): Promise<void>;
52
+ /**
53
+ * Get the current state for persistence
54
+ */
55
+ getState(): McpOAuthState;
56
+ private notifyStateUpdated;
57
+ }
58
+ /**
59
+ * Create a FileOAuthProvider for use with MCP transports.
60
+ * This is the simpler factory function for common use cases.
61
+ */
62
+ export declare function createFileOAuthProvider(options: FileOAuthProviderOptions): FileOAuthProvider;
63
+ //# sourceMappingURL=oauth-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-provider.d.ts","sourceRoot":"","sources":["../src/oauth-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AACpF,OAAO,KAAK,EACV,sBAAsB,EACtB,0BAA0B,EAC1B,mBAAmB,EACnB,WAAW,EACZ,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CACjD;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,YAAW,mBAAmB;IAC3D,OAAO,CAAC,kBAAkB,CAAqC;IAC/D,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,qBAAqB,CAAkB;IAE/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAiC;gBAErD,OAAO,EAAE,wBAAwB;IAU7C,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;;OAGG;IACH,IAAI,oBAAoB,IAAI,GAAG,GAAG,SAAS,CAE1C;IAEK,iBAAiB,IAAI,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC;IAIhE,qBAAqB,CAAC,iBAAiB,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnF,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAO/B,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3D,IAAI,cAAc,IAAI,mBAAmB,CAKxC;IAED;;;OAGG;IACH,uBAAuB,CAAC,gBAAgB,EAAE,GAAG,GAAG,IAAI;IAI9C,MAAM,IAAI,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAI1C,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpD;;OAEG;IACH,QAAQ,IAAI,aAAa;IASzB,OAAO,CAAC,kBAAkB;CAK3B;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAE5F"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * File-based OAuth provider implementation for CLI usage.
3
+ * Implements the OAuthClientProvider interface from MCP SDK.
4
+ *
5
+ * Unlike server-based implementations that use a database,
6
+ * this stores state in memory and calls onStateUpdated for persistence.
7
+ */
8
+ export class FileOAuthProvider {
9
+ _clientInformation;
10
+ _codeVerifier;
11
+ _tokens;
12
+ _redirectStartAuthUrl;
13
+ serverUrl;
14
+ redirectUri;
15
+ clientName;
16
+ onStateUpdated;
17
+ constructor(options) {
18
+ this.serverUrl = options.serverUrl;
19
+ this.redirectUri = options.redirectUri;
20
+ this.clientName = options.clientName;
21
+ this._tokens = options.tokens;
22
+ this._clientInformation = options.clientInformation;
23
+ this._codeVerifier = options.codeVerifier;
24
+ this.onStateUpdated = options.onStateUpdated;
25
+ }
26
+ get redirectUrl() {
27
+ return this.redirectUri;
28
+ }
29
+ /**
30
+ * The authorization URL to redirect the user to.
31
+ * Set by redirectToAuthorization().
32
+ */
33
+ get redirectStartAuthUrl() {
34
+ return this._redirectStartAuthUrl;
35
+ }
36
+ async clientInformation() {
37
+ return this._clientInformation;
38
+ }
39
+ async saveClientInformation(clientInformation) {
40
+ this._clientInformation = clientInformation;
41
+ this.notifyStateUpdated();
42
+ }
43
+ async codeVerifier() {
44
+ if (!this._codeVerifier) {
45
+ throw new Error("Code verifier not set");
46
+ }
47
+ return this._codeVerifier;
48
+ }
49
+ async saveCodeVerifier(codeVerifier) {
50
+ this._codeVerifier = codeVerifier;
51
+ this.notifyStateUpdated();
52
+ }
53
+ get clientMetadata() {
54
+ return {
55
+ redirect_uris: [this.redirectUri],
56
+ client_name: this.clientName,
57
+ };
58
+ }
59
+ /**
60
+ * Called by the MCP SDK when the user needs to be redirected to authorize.
61
+ * We store the URL so the CLI can open it in the browser.
62
+ */
63
+ redirectToAuthorization(authorizationUrl) {
64
+ this._redirectStartAuthUrl = authorizationUrl;
65
+ }
66
+ async tokens() {
67
+ return this._tokens;
68
+ }
69
+ async saveTokens(tokens) {
70
+ this._tokens = tokens;
71
+ this.notifyStateUpdated();
72
+ }
73
+ /**
74
+ * Get the current state for persistence
75
+ */
76
+ getState() {
77
+ return {
78
+ tokens: this._tokens,
79
+ clientInformation: this._clientInformation,
80
+ codeVerifier: this._codeVerifier,
81
+ serverUrl: this.serverUrl,
82
+ };
83
+ }
84
+ notifyStateUpdated() {
85
+ if (this.onStateUpdated) {
86
+ this.onStateUpdated(this.getState());
87
+ }
88
+ }
89
+ }
90
+ /**
91
+ * Create a FileOAuthProvider for use with MCP transports.
92
+ * This is the simpler factory function for common use cases.
93
+ */
94
+ export function createFileOAuthProvider(options) {
95
+ return new FileOAuthProvider(options);
96
+ }