mcpcac 0.0.1

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.
@@ -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
+ }
@@ -0,0 +1,77 @@
1
+ import type { OAuthTokens, OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
2
+ /**
3
+ * Persisted OAuth state for file-based storage
4
+ */
5
+ export interface McpOAuthState {
6
+ tokens?: OAuthTokens;
7
+ clientInformation?: OAuthClientInformation;
8
+ codeVerifier?: string;
9
+ serverUrl?: string;
10
+ }
11
+ /**
12
+ * OAuth configuration for addMcpCommands
13
+ */
14
+ export interface McpOAuthConfig {
15
+ /** Client name shown during OAuth consent screen */
16
+ clientName: string;
17
+ /**
18
+ * Load persisted OAuth state from storage (e.g., config file)
19
+ */
20
+ load: () => McpOAuthState | undefined;
21
+ /**
22
+ * Save OAuth state to storage. Called after successful auth or token refresh.
23
+ * Pass undefined to clear the state (logout).
24
+ */
25
+ save: (state: McpOAuthState | undefined) => void;
26
+ /**
27
+ * Called with the authorization URL. Default behavior opens the browser.
28
+ * Override to customize (e.g., just print the URL).
29
+ */
30
+ onAuthUrl?: (url: string) => void;
31
+ /**
32
+ * Called on successful authentication
33
+ */
34
+ onAuthSuccess?: () => void;
35
+ /**
36
+ * Called on authentication error
37
+ */
38
+ onAuthError?: (error: string) => void;
39
+ }
40
+ /**
41
+ * Result of startOAuthFlow
42
+ */
43
+ export interface OAuthFlowResult {
44
+ success: boolean;
45
+ state?: McpOAuthState;
46
+ error?: string;
47
+ }
48
+ /**
49
+ * Options for starting OAuth flow
50
+ */
51
+ export interface StartOAuthFlowOptions {
52
+ serverUrl: string;
53
+ clientName: string;
54
+ /** Existing OAuth state (for re-auth scenarios) */
55
+ existingState?: McpOAuthState;
56
+ /** Called with auth URL, default opens browser */
57
+ onAuthUrl?: (url: string) => void;
58
+ /** Timeout in ms waiting for callback, default 5 minutes */
59
+ timeout?: number;
60
+ }
61
+ /**
62
+ * Options for the local callback server
63
+ */
64
+ export interface CallbackServerOptions {
65
+ /** Called when server starts with the redirect URI */
66
+ onReady?: (redirectUri: string) => void;
67
+ /** Timeout in ms, default 5 minutes */
68
+ timeout?: number;
69
+ }
70
+ /**
71
+ * Result from callback server
72
+ */
73
+ export interface CallbackResult {
74
+ code: string;
75
+ state: string;
76
+ }
77
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAC;AAEpG;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,IAAI,EAAE,MAAM,aAAa,GAAG,SAAS,CAAC;IAEtC;;;OAGG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,IAAI,CAAC;IAEjD;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAElC;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAE3B;;OAEG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,kDAAkD;IAClD,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,sDAAsD;IACtD,OAAO,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "mcpcac",
3
+ "version": "0.0.1",
4
+ "description": "Dynamically generate CLI commands from MCP server tools",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./src/index.js": {
14
+ "types": "./src/index.ts",
15
+ "import": "./src/index.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
22
+ "keywords": [
23
+ "mcp",
24
+ "cli",
25
+ "cac"
26
+ ],
27
+ "author": "Tommaso De Rossi, morse <beats.by.morse@gmail.com>",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.0.0"
31
+ },
32
+ "peerDependencies": {
33
+ "@xmorse/cac": "*"
34
+ },
35
+ "devDependencies": {
36
+ "@xmorse/cac": "^6.0.7"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "watch": "tsc -w"
41
+ }
42
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,138 @@
1
+ import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import { FileOAuthProvider } from "./oauth-provider.js";
3
+ import { startCallbackServer } from "./local-callback-server.js";
4
+ import type { McpOAuthState, OAuthFlowResult, StartOAuthFlowOptions } from "./types.js";
5
+
6
+ /**
7
+ * Open a URL in the default browser.
8
+ * Uses platform-specific commands.
9
+ */
10
+ async function openBrowser(url: string): Promise<void> {
11
+ const { exec } = await import("node:child_process");
12
+ const { promisify } = await import("node:util");
13
+ const execAsync = promisify(exec);
14
+
15
+ const platform = process.platform;
16
+ const command = (() => {
17
+ if (platform === "darwin") {
18
+ return `open "${url}"`;
19
+ }
20
+ if (platform === "win32") {
21
+ return `start "" "${url}"`;
22
+ }
23
+ // Linux and others
24
+ return `xdg-open "${url}"`;
25
+ })();
26
+
27
+ await execAsync(command);
28
+ }
29
+
30
+ /**
31
+ * Start the OAuth flow for an MCP server.
32
+ * This is an internal function - consumers should not call this directly.
33
+ * It is automatically triggered by addMcpCommands when a 401 error occurs.
34
+ *
35
+ * This function:
36
+ * 1. Starts a local callback server on a random port
37
+ * 2. Initiates OAuth with the MCP server
38
+ * 3. Opens the browser for user authorization
39
+ * 4. Waits for the callback with the authorization code
40
+ * 5. Exchanges the code for tokens
41
+ * 6. Returns the OAuth state for persistence
42
+ */
43
+ export async function startOAuthFlow(options: StartOAuthFlowOptions): Promise<OAuthFlowResult> {
44
+ const { serverUrl, clientName, existingState, onAuthUrl, timeout } = options;
45
+
46
+ // Start local callback server on random port
47
+ const callbackServer = await startCallbackServer({ timeout });
48
+ const { redirectUri, waitForCallback, close } = callbackServer;
49
+
50
+ try {
51
+ // Create OAuth provider with the dynamic redirect URI
52
+ const oauthProvider = new FileOAuthProvider({
53
+ serverUrl,
54
+ redirectUri,
55
+ clientName,
56
+ tokens: existingState?.tokens,
57
+ clientInformation: existingState?.clientInformation,
58
+ codeVerifier: existingState?.codeVerifier,
59
+ });
60
+
61
+ // Start the OAuth flow - this will trigger dynamic client registration
62
+ // and set the authorization URL on the provider
63
+ const authResult = await auth(oauthProvider, { serverUrl });
64
+
65
+ if (authResult !== "REDIRECT") {
66
+ // Auth succeeded without redirect (had valid tokens)
67
+ close();
68
+ return {
69
+ success: true,
70
+ state: oauthProvider.getState(),
71
+ };
72
+ }
73
+
74
+ // Get the authorization URL
75
+ const authUrl = oauthProvider.redirectStartAuthUrl;
76
+ if (!authUrl) {
77
+ close();
78
+ return {
79
+ success: false,
80
+ error: "No authorization URL returned from OAuth flow",
81
+ };
82
+ }
83
+
84
+ // Open browser or call custom handler
85
+ const authUrlString = authUrl.toString();
86
+ if (onAuthUrl) {
87
+ onAuthUrl(authUrlString);
88
+ } else {
89
+ await openBrowser(authUrlString);
90
+ }
91
+
92
+ // Wait for the callback
93
+ const callback = await waitForCallback();
94
+
95
+ // Complete the OAuth flow by exchanging the code for tokens
96
+ const finalResult = await auth(oauthProvider, {
97
+ serverUrl,
98
+ authorizationCode: callback.code,
99
+ });
100
+
101
+ if (finalResult === "REDIRECT") {
102
+ return {
103
+ success: false,
104
+ error: "Unexpected redirect after code exchange",
105
+ };
106
+ }
107
+
108
+ return {
109
+ success: true,
110
+ state: oauthProvider.getState(),
111
+ };
112
+ } catch (err) {
113
+ close();
114
+ return {
115
+ success: false,
116
+ error: err instanceof Error ? err.message : String(err),
117
+ };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Check if an error indicates authentication is required.
123
+ * Internal function used by addMcpCommands.
124
+ */
125
+ export function isAuthRequiredError(err: unknown): boolean {
126
+ if (!(err instanceof Error)) {
127
+ return false;
128
+ }
129
+ const message = err.message.toLowerCase();
130
+ return (
131
+ message.includes("401") ||
132
+ message.includes("unauthorized") ||
133
+ message.includes("authentication required") ||
134
+ message.includes("not authenticated") ||
135
+ message.includes("invalid_token") ||
136
+ message.includes("missing or invalid access token")
137
+ );
138
+ }