mcp-use 0.1.19 → 0.2.0
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/src/adapters/langchain_adapter.d.ts.map +1 -1
- package/dist/src/adapters/langchain_adapter.js +1 -42
- package/dist/src/browser.d.ts +49 -0
- package/dist/src/browser.d.ts.map +1 -0
- package/dist/src/browser.js +75 -0
- package/dist/src/client/base.d.ts +32 -0
- package/dist/src/client/base.d.ts.map +1 -0
- package/dist/src/client/base.js +119 -0
- package/dist/src/client.d.ts +19 -16
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +24 -107
- package/dist/src/logging.d.ts +1 -1
- package/dist/src/logging.d.ts.map +1 -1
- package/dist/src/logging.js +31 -16
- package/dist/src/managers/server_manager.js +1 -1
- package/dist/src/oauth-helper.d.ts +135 -0
- package/dist/src/oauth-helper.d.ts.map +1 -0
- package/dist/src/oauth-helper.js +427 -0
- package/package.json +6 -1
- package/dist/examples/add_server_tool.d.ts +0 -8
- package/dist/examples/add_server_tool.d.ts.map +0 -1
- package/dist/examples/add_server_tool.js +0 -79
- package/dist/examples/ai_sdk_example.d.ts +0 -23
- package/dist/examples/ai_sdk_example.d.ts.map +0 -1
- package/dist/examples/ai_sdk_example.js +0 -213
- package/dist/examples/airbnb_use.d.ts +0 -10
- package/dist/examples/airbnb_use.d.ts.map +0 -1
- package/dist/examples/airbnb_use.js +0 -43
- package/dist/examples/blender_use.d.ts +0 -15
- package/dist/examples/blender_use.d.ts.map +0 -1
- package/dist/examples/blender_use.js +0 -39
- package/dist/examples/browser_use.d.ts +0 -10
- package/dist/examples/browser_use.d.ts.map +0 -1
- package/dist/examples/browser_use.js +0 -46
- package/dist/examples/chat_example.d.ts +0 -10
- package/dist/examples/chat_example.d.ts.map +0 -1
- package/dist/examples/chat_example.js +0 -86
- package/dist/examples/filesystem_use.d.ts +0 -11
- package/dist/examples/filesystem_use.d.ts.map +0 -1
- package/dist/examples/filesystem_use.js +0 -43
- package/dist/examples/http_example.d.ts +0 -18
- package/dist/examples/http_example.d.ts.map +0 -1
- package/dist/examples/http_example.js +0 -37
- package/dist/examples/mcp_everything.d.ts +0 -6
- package/dist/examples/mcp_everything.d.ts.map +0 -1
- package/dist/examples/mcp_everything.js +0 -25
- package/dist/examples/multi_server_example.d.ts +0 -10
- package/dist/examples/multi_server_example.d.ts.map +0 -1
- package/dist/examples/multi_server_example.js +0 -51
- package/dist/examples/observability.d.ts +0 -6
- package/dist/examples/observability.d.ts.map +0 -1
- package/dist/examples/observability.js +0 -50
- package/dist/examples/stream_example.d.ts +0 -12
- package/dist/examples/stream_example.d.ts.map +0 -1
- package/dist/examples/stream_example.js +0 -198
- package/dist/examples/structured_output.d.ts +0 -9
- package/dist/examples/structured_output.d.ts.map +0 -1
- package/dist/examples/structured_output.js +0 -95
package/dist/src/logging.js
CHANGED
@@ -1,6 +1,19 @@
|
|
1
|
-
import fs from 'node:fs';
|
2
|
-
import path from 'node:path';
|
3
1
|
import { createLogger, format, transports } from 'winston';
|
2
|
+
// Conditional imports for Node.js-only modules
|
3
|
+
async function getNodeModules() {
|
4
|
+
if (typeof process !== 'undefined' && process.platform) {
|
5
|
+
try {
|
6
|
+
// Use dynamic imports for Node.js environments
|
7
|
+
const fs = await import('node:fs');
|
8
|
+
const path = await import('node:path');
|
9
|
+
return { fs: fs.default, path: path.default };
|
10
|
+
}
|
11
|
+
catch {
|
12
|
+
return { fs: null, path: null };
|
13
|
+
}
|
14
|
+
}
|
15
|
+
return { fs: null, path: null };
|
16
|
+
}
|
4
17
|
const { combine, timestamp, label, printf, colorize, splat } = format;
|
5
18
|
const DEFAULT_LOGGER_NAME = 'mcp-use';
|
6
19
|
// Environment detection function (similar to telemetry)
|
@@ -19,8 +32,7 @@ function isNodeJSEnvironment() {
|
|
19
32
|
&& typeof process.platform !== 'undefined'
|
20
33
|
&& typeof __dirname !== 'undefined');
|
21
34
|
// Check for Node.js modules
|
22
|
-
const hasNodeModules = (typeof
|
23
|
-
&& typeof createLogger === 'function');
|
35
|
+
const hasNodeModules = (typeof createLogger === 'function');
|
24
36
|
return hasNodeGlobals && hasNodeModules;
|
25
37
|
}
|
26
38
|
catch {
|
@@ -57,27 +69,27 @@ class SimpleConsoleLogger {
|
|
57
69
|
}
|
58
70
|
info(message) {
|
59
71
|
if (this.shouldLog('info')) {
|
60
|
-
console.info(this.formatMessage('info', message));
|
72
|
+
console.info(this.formatMessage('info', message));
|
61
73
|
}
|
62
74
|
}
|
63
75
|
debug(message) {
|
64
76
|
if (this.shouldLog('debug')) {
|
65
|
-
console.debug(this.formatMessage('debug', message));
|
77
|
+
console.debug(this.formatMessage('debug', message));
|
66
78
|
}
|
67
79
|
}
|
68
80
|
http(message) {
|
69
81
|
if (this.shouldLog('http')) {
|
70
|
-
console.log(this.formatMessage('http', message));
|
82
|
+
console.log(this.formatMessage('http', message));
|
71
83
|
}
|
72
84
|
}
|
73
85
|
verbose(message) {
|
74
86
|
if (this.shouldLog('verbose')) {
|
75
|
-
console.log(this.formatMessage('verbose', message));
|
87
|
+
console.log(this.formatMessage('verbose', message));
|
76
88
|
}
|
77
89
|
}
|
78
90
|
silly(message) {
|
79
91
|
if (this.shouldLog('silly')) {
|
80
|
-
console.log(this.formatMessage('silly', message));
|
92
|
+
console.log(this.formatMessage('silly', message));
|
81
93
|
}
|
82
94
|
}
|
83
95
|
// Make it compatible with Winston interface
|
@@ -144,13 +156,15 @@ export class Logger {
|
|
144
156
|
return minimalFormatter;
|
145
157
|
}
|
146
158
|
}
|
147
|
-
static configure(options = {}) {
|
159
|
+
static async configure(options = {}) {
|
148
160
|
const { level, console = true, file, format = 'minimal' } = options;
|
149
161
|
const debugEnv = (typeof process !== 'undefined' && process.env?.DEBUG) || undefined;
|
150
162
|
const resolvedLevel = level ?? resolveLevel(debugEnv);
|
151
163
|
this.currentFormat = format;
|
152
164
|
const root = this.get();
|
153
165
|
root.level = resolvedLevel;
|
166
|
+
// Winston-specific configuration for Node.js environments
|
167
|
+
const winstonRoot = root;
|
154
168
|
// For non-Node.js environments, just update the level
|
155
169
|
if (!isNodeJSEnvironment()) {
|
156
170
|
Object.values(this.simpleInstances).forEach((logger) => {
|
@@ -158,18 +172,19 @@ export class Logger {
|
|
158
172
|
});
|
159
173
|
return;
|
160
174
|
}
|
161
|
-
// Winston-specific configuration for Node.js environments
|
162
|
-
const winstonRoot = root;
|
163
175
|
winstonRoot.clear();
|
164
176
|
if (console) {
|
165
177
|
winstonRoot.add(new transports.Console());
|
166
178
|
}
|
167
179
|
if (file) {
|
168
|
-
const
|
169
|
-
if (
|
170
|
-
|
180
|
+
const { fs: nodeFs, path: nodePath } = await getNodeModules();
|
181
|
+
if (nodeFs && nodePath) {
|
182
|
+
const dir = nodePath.dirname(nodePath.resolve(file));
|
183
|
+
if (!nodeFs.existsSync(dir)) {
|
184
|
+
nodeFs.mkdirSync(dir, { recursive: true });
|
185
|
+
}
|
186
|
+
winstonRoot.add(new transports.File({ filename: file }));
|
171
187
|
}
|
172
|
-
winstonRoot.add(new transports.File({ filename: file }));
|
173
188
|
}
|
174
189
|
// Update all existing Winston loggers with new format
|
175
190
|
Object.values(this.instances).forEach((logger) => {
|
@@ -36,7 +36,7 @@ export class ServerManager {
|
|
36
36
|
'Active': this.activeServer === name ? '✅' : '❌',
|
37
37
|
}));
|
38
38
|
logger.info(`Server Manager State: [${context}]`);
|
39
|
-
console.table(tableData);
|
39
|
+
console.table(tableData);
|
40
40
|
}
|
41
41
|
initialize() {
|
42
42
|
const serverNames = this.client.getServerNames?.();
|
@@ -0,0 +1,135 @@
|
|
1
|
+
/**
|
2
|
+
* OAuth helper for browser-based MCP authentication
|
3
|
+
*
|
4
|
+
* This helper provides OAuth 2.0 authorization code flow support for MCP servers
|
5
|
+
* that require authentication, such as Linear's MCP server.
|
6
|
+
*/
|
7
|
+
export interface OAuthConfig {
|
8
|
+
clientId?: string;
|
9
|
+
redirectUri: string;
|
10
|
+
scope?: string;
|
11
|
+
state?: string;
|
12
|
+
clientName?: string;
|
13
|
+
}
|
14
|
+
export interface OAuthDiscovery {
|
15
|
+
issuer: string;
|
16
|
+
authorization_endpoint: string;
|
17
|
+
token_endpoint: string;
|
18
|
+
registration_endpoint?: string;
|
19
|
+
response_types_supported: string[];
|
20
|
+
grant_types_supported: string[];
|
21
|
+
code_challenge_methods_supported: string[];
|
22
|
+
token_endpoint_auth_methods_supported?: string[];
|
23
|
+
}
|
24
|
+
export interface ClientRegistration {
|
25
|
+
client_id: string;
|
26
|
+
client_secret?: string;
|
27
|
+
registration_access_token?: string;
|
28
|
+
registration_client_uri?: string;
|
29
|
+
client_id_issued_at?: number;
|
30
|
+
client_secret_expires_at?: number;
|
31
|
+
}
|
32
|
+
export interface OAuthResult {
|
33
|
+
access_token: string;
|
34
|
+
token_type: string;
|
35
|
+
expires_at?: number | null;
|
36
|
+
refresh_token?: string | null;
|
37
|
+
scope?: string | null;
|
38
|
+
}
|
39
|
+
export interface OAuthState {
|
40
|
+
isRequired: boolean;
|
41
|
+
isAuthenticated: boolean;
|
42
|
+
isAuthenticating: boolean;
|
43
|
+
isCompletingOAuth: boolean;
|
44
|
+
authError: string | null;
|
45
|
+
oauthTokens: OAuthResult | null;
|
46
|
+
}
|
47
|
+
export declare class OAuthHelper {
|
48
|
+
private config;
|
49
|
+
private discovery?;
|
50
|
+
private state;
|
51
|
+
private clientRegistration?;
|
52
|
+
private serverUrl?;
|
53
|
+
private storageKey;
|
54
|
+
constructor(config: OAuthConfig);
|
55
|
+
/**
|
56
|
+
* Get current OAuth state
|
57
|
+
*/
|
58
|
+
getState(): OAuthState;
|
59
|
+
/**
|
60
|
+
* Load client registration from localStorage
|
61
|
+
*/
|
62
|
+
private loadClientRegistration;
|
63
|
+
/**
|
64
|
+
* Save client registration to localStorage
|
65
|
+
*/
|
66
|
+
private saveClientRegistration;
|
67
|
+
/**
|
68
|
+
* Check if a server requires authentication by pinging the URL
|
69
|
+
*/
|
70
|
+
checkAuthRequired(serverUrl: string): Promise<boolean>;
|
71
|
+
/**
|
72
|
+
* Fallback heuristics for determining auth requirements when direct checking fails
|
73
|
+
*/
|
74
|
+
private checkAuthByHeuristics;
|
75
|
+
/**
|
76
|
+
* Discover OAuth configuration from a server
|
77
|
+
*/
|
78
|
+
discoverOAuthConfig(serverUrl: string): Promise<OAuthDiscovery>;
|
79
|
+
/**
|
80
|
+
* Register a new OAuth client dynamically
|
81
|
+
*/
|
82
|
+
registerClient(serverUrl: string): Promise<ClientRegistration>;
|
83
|
+
/**
|
84
|
+
* Generate authorization URL for OAuth flow
|
85
|
+
*/
|
86
|
+
generateAuthUrl(serverUrl: string, additionalParams?: Record<string, string>): string;
|
87
|
+
/**
|
88
|
+
* Exchange authorization code for access token
|
89
|
+
*/
|
90
|
+
exchangeCodeForToken(serverUrl: string, code: string, codeVerifier?: string): Promise<OAuthResult>;
|
91
|
+
/**
|
92
|
+
* Handle OAuth callback and extract authorization code
|
93
|
+
*/
|
94
|
+
handleCallback(): {
|
95
|
+
code: string;
|
96
|
+
state: string;
|
97
|
+
} | null;
|
98
|
+
/**
|
99
|
+
* Start OAuth flow by opening popup window (similar to your implementation)
|
100
|
+
*/
|
101
|
+
startOAuthFlow(serverUrl: string): Promise<void>;
|
102
|
+
/**
|
103
|
+
* Complete OAuth flow by exchanging code for token
|
104
|
+
*/
|
105
|
+
completeOAuthFlow(serverUrl: string, code: string): Promise<OAuthResult>;
|
106
|
+
/**
|
107
|
+
* Reset authentication state
|
108
|
+
*/
|
109
|
+
resetAuth(): void;
|
110
|
+
/**
|
111
|
+
* Set OAuth state (internal method)
|
112
|
+
*/
|
113
|
+
private setState;
|
114
|
+
/**
|
115
|
+
* Generate a random state parameter for CSRF protection
|
116
|
+
*/
|
117
|
+
private generateState;
|
118
|
+
}
|
119
|
+
/**
|
120
|
+
* Linear-specific OAuth configuration
|
121
|
+
*/
|
122
|
+
export declare const LINEAR_OAUTH_CONFIG: OAuthConfig;
|
123
|
+
/**
|
124
|
+
* Helper function to create OAuth-enabled MCP configuration
|
125
|
+
*/
|
126
|
+
export declare function createOAuthMCPConfig(serverUrl: string, accessToken: string): {
|
127
|
+
mcpServers: {
|
128
|
+
linear: {
|
129
|
+
url: string;
|
130
|
+
authToken: string;
|
131
|
+
transport: string;
|
132
|
+
};
|
133
|
+
};
|
134
|
+
};
|
135
|
+
//# sourceMappingURL=oauth-helper.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"oauth-helper.d.ts","sourceRoot":"","sources":["../../src/oauth-helper.ts"],"names":[],"mappings":"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"}
|
@@ -0,0 +1,427 @@
|
|
1
|
+
/**
|
2
|
+
* OAuth helper for browser-based MCP authentication
|
3
|
+
*
|
4
|
+
* This helper provides OAuth 2.0 authorization code flow support for MCP servers
|
5
|
+
* that require authentication, such as Linear's MCP server.
|
6
|
+
*/
|
7
|
+
export class OAuthHelper {
|
8
|
+
config;
|
9
|
+
discovery;
|
10
|
+
state;
|
11
|
+
clientRegistration;
|
12
|
+
serverUrl;
|
13
|
+
storageKey;
|
14
|
+
constructor(config) {
|
15
|
+
this.config = config;
|
16
|
+
this.storageKey = `mcp-oauth-${btoa(config.redirectUri)}`;
|
17
|
+
this.state = {
|
18
|
+
isRequired: false,
|
19
|
+
isAuthenticated: false,
|
20
|
+
isAuthenticating: false,
|
21
|
+
isCompletingOAuth: false,
|
22
|
+
authError: null,
|
23
|
+
oauthTokens: null,
|
24
|
+
};
|
25
|
+
// Load persisted client registration
|
26
|
+
this.loadClientRegistration();
|
27
|
+
}
|
28
|
+
/**
|
29
|
+
* Get current OAuth state
|
30
|
+
*/
|
31
|
+
getState() {
|
32
|
+
return { ...this.state };
|
33
|
+
}
|
34
|
+
/**
|
35
|
+
* Load client registration from localStorage
|
36
|
+
*/
|
37
|
+
loadClientRegistration() {
|
38
|
+
try {
|
39
|
+
const stored = localStorage.getItem(this.storageKey);
|
40
|
+
if (stored) {
|
41
|
+
const data = JSON.parse(stored);
|
42
|
+
this.clientRegistration = data.clientRegistration;
|
43
|
+
this.serverUrl = data.serverUrl;
|
44
|
+
console.log('🔄 [OAuthHelper] Loaded persisted client registration:', {
|
45
|
+
client_id: this.clientRegistration?.client_id,
|
46
|
+
server: this.serverUrl,
|
47
|
+
});
|
48
|
+
}
|
49
|
+
}
|
50
|
+
catch (error) {
|
51
|
+
console.warn('⚠️ [OAuthHelper] Failed to load client registration:', error);
|
52
|
+
}
|
53
|
+
}
|
54
|
+
/**
|
55
|
+
* Save client registration to localStorage
|
56
|
+
*/
|
57
|
+
saveClientRegistration() {
|
58
|
+
try {
|
59
|
+
const data = {
|
60
|
+
clientRegistration: this.clientRegistration,
|
61
|
+
serverUrl: this.serverUrl,
|
62
|
+
};
|
63
|
+
localStorage.setItem(this.storageKey, JSON.stringify(data));
|
64
|
+
console.log('💾 [OAuthHelper] Saved client registration to localStorage');
|
65
|
+
}
|
66
|
+
catch (error) {
|
67
|
+
console.warn('⚠️ [OAuthHelper] Failed to save client registration:', error);
|
68
|
+
}
|
69
|
+
}
|
70
|
+
/**
|
71
|
+
* Check if a server requires authentication by pinging the URL
|
72
|
+
*/
|
73
|
+
async checkAuthRequired(serverUrl) {
|
74
|
+
console.log('🔍 [OAuthHelper] Checking auth requirement for:', serverUrl);
|
75
|
+
try {
|
76
|
+
const response = await fetch(serverUrl, {
|
77
|
+
method: 'GET',
|
78
|
+
headers: {
|
79
|
+
'Accept': 'text/event-stream',
|
80
|
+
'Cache-Control': 'no-cache',
|
81
|
+
},
|
82
|
+
redirect: 'manual',
|
83
|
+
signal: AbortSignal.timeout(10000), // 10 second timeout
|
84
|
+
});
|
85
|
+
console.log('🔍 [OAuthHelper] Auth check response:', {
|
86
|
+
status: response.status,
|
87
|
+
statusText: response.statusText,
|
88
|
+
url: serverUrl,
|
89
|
+
});
|
90
|
+
// 401 Unauthorized, 403 Forbidden, or 400 Bad Request means auth is required
|
91
|
+
if (response.status === 401 || response.status === 403 || response.status === 400) {
|
92
|
+
console.log('🔐 [OAuthHelper] Authentication required for:', serverUrl);
|
93
|
+
return true;
|
94
|
+
}
|
95
|
+
// Any other response (200, 404, 500, etc.) means no auth required
|
96
|
+
console.log('✅ [OAuthHelper] No authentication required for:', serverUrl);
|
97
|
+
return false;
|
98
|
+
}
|
99
|
+
catch (error) {
|
100
|
+
console.warn('⚠️ [OAuthHelper] Could not check auth requirement for:', serverUrl, error);
|
101
|
+
// Handle specific error types
|
102
|
+
if (error.name === 'TypeError'
|
103
|
+
&& (error.message?.includes('CORS') || error.message?.includes('Failed to fetch'))) {
|
104
|
+
console.log('🔍 [OAuthHelper] CORS blocked direct check, using heuristics for:', serverUrl);
|
105
|
+
return this.checkAuthByHeuristics(serverUrl);
|
106
|
+
}
|
107
|
+
if (error.name === 'AbortError') {
|
108
|
+
console.log('⏰ [OAuthHelper] Request timeout, assuming no auth required for:', serverUrl);
|
109
|
+
return false;
|
110
|
+
}
|
111
|
+
// If we can't reach the server at all, try heuristics
|
112
|
+
return this.checkAuthByHeuristics(serverUrl);
|
113
|
+
}
|
114
|
+
}
|
115
|
+
/**
|
116
|
+
* Fallback heuristics for determining auth requirements when direct checking fails
|
117
|
+
*/
|
118
|
+
checkAuthByHeuristics(serverUrl) {
|
119
|
+
console.log('🔍 [OAuthHelper] Using heuristics to determine auth for:', serverUrl);
|
120
|
+
// Known patterns that typically require auth
|
121
|
+
const authRequiredPatterns = [
|
122
|
+
/api\.githubcopilot\.com/i, // GitHub Copilot
|
123
|
+
/api\.github\.com/i, // GitHub API
|
124
|
+
/.*\.googleapis\.com/i, // Google APIs
|
125
|
+
/api\.openai\.com/i, // OpenAI
|
126
|
+
/api\.anthropic\.com/i, // Anthropic
|
127
|
+
/.*\.atlassian\.net/i, // Atlassian (Jira, Confluence)
|
128
|
+
/.*\.slack\.com/i, // Slack
|
129
|
+
/api\.notion\.com/i, // Notion
|
130
|
+
/api\.linear\.app/i, // Linear
|
131
|
+
];
|
132
|
+
// Known patterns that typically don't require auth (public MCP servers)
|
133
|
+
const noAuthPatterns = [
|
134
|
+
/localhost/i, // Local development
|
135
|
+
/127\.0\.0\.1/, // Local development
|
136
|
+
/\.local/i, // Local development
|
137
|
+
/mcp\..*\.com/i, // Generic MCP server pattern (often public)
|
138
|
+
];
|
139
|
+
// Check no-auth patterns first
|
140
|
+
for (const pattern of noAuthPatterns) {
|
141
|
+
if (pattern.test(serverUrl)) {
|
142
|
+
console.log('✅ [OAuthHelper] Heuristic: No auth required (matches no-auth pattern):', serverUrl);
|
143
|
+
return false;
|
144
|
+
}
|
145
|
+
}
|
146
|
+
// Check auth-required patterns
|
147
|
+
for (const pattern of authRequiredPatterns) {
|
148
|
+
if (pattern.test(serverUrl)) {
|
149
|
+
console.log('🔐 [OAuthHelper] Heuristic: Auth required (matches auth pattern):', serverUrl);
|
150
|
+
return true;
|
151
|
+
}
|
152
|
+
}
|
153
|
+
// Default: assume no auth required for unknown patterns
|
154
|
+
console.log('❓ [OAuthHelper] Heuristic: Unknown pattern, assuming no auth required:', serverUrl);
|
155
|
+
return false;
|
156
|
+
}
|
157
|
+
/**
|
158
|
+
* Discover OAuth configuration from a server
|
159
|
+
*/
|
160
|
+
async discoverOAuthConfig(serverUrl) {
|
161
|
+
try {
|
162
|
+
const discoveryUrl = `${serverUrl}/.well-known/oauth-authorization-server`;
|
163
|
+
const response = await fetch(discoveryUrl);
|
164
|
+
if (!response.ok) {
|
165
|
+
throw new Error(`OAuth discovery failed: ${response.status} ${response.statusText}`);
|
166
|
+
}
|
167
|
+
this.discovery = await response.json();
|
168
|
+
return this.discovery;
|
169
|
+
}
|
170
|
+
catch (error) {
|
171
|
+
throw new Error(`Failed to discover OAuth configuration: ${error}`);
|
172
|
+
}
|
173
|
+
}
|
174
|
+
/**
|
175
|
+
* Register a new OAuth client dynamically
|
176
|
+
*/
|
177
|
+
async registerClient(serverUrl) {
|
178
|
+
if (!this.discovery) {
|
179
|
+
throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.');
|
180
|
+
}
|
181
|
+
if (!this.discovery.registration_endpoint) {
|
182
|
+
throw new Error('Server does not support dynamic client registration');
|
183
|
+
}
|
184
|
+
try {
|
185
|
+
const registrationData = {
|
186
|
+
client_name: this.config.clientName || 'MCP Use Example',
|
187
|
+
redirect_uris: [this.config.redirectUri],
|
188
|
+
grant_types: ['authorization_code'],
|
189
|
+
response_types: ['code'],
|
190
|
+
token_endpoint_auth_method: 'none', // Use public client (no secret)
|
191
|
+
scope: this.config.scope || 'read write',
|
192
|
+
};
|
193
|
+
console.log('🔐 [OAuthHelper] Registering OAuth client dynamically:', {
|
194
|
+
registration_endpoint: this.discovery.registration_endpoint,
|
195
|
+
client_name: registrationData.client_name,
|
196
|
+
redirect_uri: this.config.redirectUri,
|
197
|
+
});
|
198
|
+
const response = await fetch(this.discovery.registration_endpoint, {
|
199
|
+
method: 'POST',
|
200
|
+
headers: {
|
201
|
+
'Content-Type': 'application/json',
|
202
|
+
},
|
203
|
+
body: JSON.stringify(registrationData),
|
204
|
+
});
|
205
|
+
if (!response.ok) {
|
206
|
+
const errorText = await response.text();
|
207
|
+
throw new Error(`Client registration failed: ${response.status} ${response.statusText} - ${errorText}`);
|
208
|
+
}
|
209
|
+
this.clientRegistration = await response.json();
|
210
|
+
this.serverUrl = serverUrl;
|
211
|
+
this.saveClientRegistration();
|
212
|
+
console.log('✅ [OAuthHelper] Client registered successfully:', {
|
213
|
+
client_id: this.clientRegistration.client_id,
|
214
|
+
client_secret: this.clientRegistration.client_secret ? '***' : 'none',
|
215
|
+
});
|
216
|
+
return this.clientRegistration;
|
217
|
+
}
|
218
|
+
catch (error) {
|
219
|
+
console.error('❌ [OAuthHelper] Client registration failed:', error);
|
220
|
+
throw new Error(`Failed to register OAuth client: ${error}`);
|
221
|
+
}
|
222
|
+
}
|
223
|
+
/**
|
224
|
+
* Generate authorization URL for OAuth flow
|
225
|
+
*/
|
226
|
+
generateAuthUrl(serverUrl, additionalParams) {
|
227
|
+
if (!this.discovery) {
|
228
|
+
throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.');
|
229
|
+
}
|
230
|
+
if (!this.clientRegistration) {
|
231
|
+
throw new Error('Client not registered. Call registerClient first.');
|
232
|
+
}
|
233
|
+
const params = new URLSearchParams({
|
234
|
+
client_id: this.clientRegistration.client_id,
|
235
|
+
redirect_uri: this.config.redirectUri,
|
236
|
+
response_type: 'code',
|
237
|
+
scope: this.config.scope || 'read',
|
238
|
+
state: this.config.state || this.generateState(),
|
239
|
+
...additionalParams,
|
240
|
+
});
|
241
|
+
return `${this.discovery.authorization_endpoint}?${params.toString()}`;
|
242
|
+
}
|
243
|
+
/**
|
244
|
+
* Exchange authorization code for access token
|
245
|
+
*/
|
246
|
+
async exchangeCodeForToken(serverUrl, code, codeVerifier) {
|
247
|
+
if (!this.discovery) {
|
248
|
+
throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.');
|
249
|
+
}
|
250
|
+
if (!this.clientRegistration) {
|
251
|
+
throw new Error('Client not registered. Call registerClient first.');
|
252
|
+
}
|
253
|
+
const body = new URLSearchParams({
|
254
|
+
grant_type: 'authorization_code',
|
255
|
+
client_id: this.clientRegistration.client_id,
|
256
|
+
code,
|
257
|
+
redirect_uri: this.config.redirectUri,
|
258
|
+
});
|
259
|
+
if (codeVerifier) {
|
260
|
+
body.append('code_verifier', codeVerifier);
|
261
|
+
}
|
262
|
+
const response = await fetch(this.discovery.token_endpoint, {
|
263
|
+
method: 'POST',
|
264
|
+
headers: {
|
265
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
266
|
+
},
|
267
|
+
body: body.toString(),
|
268
|
+
});
|
269
|
+
if (!response.ok) {
|
270
|
+
const error = await response.text();
|
271
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${error}`);
|
272
|
+
}
|
273
|
+
return await response.json();
|
274
|
+
}
|
275
|
+
/**
|
276
|
+
* Handle OAuth callback and extract authorization code
|
277
|
+
*/
|
278
|
+
handleCallback() {
|
279
|
+
const urlParams = new URLSearchParams(window.location.search);
|
280
|
+
const code = urlParams.get('code');
|
281
|
+
const state = urlParams.get('state');
|
282
|
+
if (!code || !state) {
|
283
|
+
return null;
|
284
|
+
}
|
285
|
+
// Clean up URL
|
286
|
+
const url = new URL(window.location.href);
|
287
|
+
url.searchParams.delete('code');
|
288
|
+
url.searchParams.delete('state');
|
289
|
+
window.history.replaceState({}, '', url.toString());
|
290
|
+
return { code, state };
|
291
|
+
}
|
292
|
+
/**
|
293
|
+
* Start OAuth flow by opening popup window (similar to your implementation)
|
294
|
+
*/
|
295
|
+
async startOAuthFlow(serverUrl) {
|
296
|
+
this.setState({
|
297
|
+
isAuthenticating: true,
|
298
|
+
authError: null,
|
299
|
+
});
|
300
|
+
try {
|
301
|
+
// Step 1: Discover OAuth configuration
|
302
|
+
await this.discoverOAuthConfig(serverUrl);
|
303
|
+
// Step 2: Register client dynamically (if not already registered)
|
304
|
+
if (!this.clientRegistration) {
|
305
|
+
await this.registerClient(serverUrl);
|
306
|
+
}
|
307
|
+
// Step 3: Generate authorization URL
|
308
|
+
const authUrl = this.generateAuthUrl(serverUrl);
|
309
|
+
// Step 4: Open popup window for authentication
|
310
|
+
const authWindow = window.open(authUrl, 'mcp-oauth', 'width=500,height=600,scrollbars=yes,resizable=yes,status=yes,location=yes');
|
311
|
+
if (!authWindow) {
|
312
|
+
throw new Error('Failed to open authentication window. Please allow popups for this site and try again.');
|
313
|
+
}
|
314
|
+
console.log('✅ [OAuthHelper] OAuth popup opened successfully');
|
315
|
+
}
|
316
|
+
catch (error) {
|
317
|
+
console.error('❌ [OAuthHelper] Failed to start OAuth flow:', error);
|
318
|
+
this.setState({
|
319
|
+
isAuthenticating: false,
|
320
|
+
authError: error instanceof Error ? error.message : 'Failed to start authentication',
|
321
|
+
});
|
322
|
+
throw error;
|
323
|
+
}
|
324
|
+
}
|
325
|
+
/**
|
326
|
+
* Complete OAuth flow by exchanging code for token
|
327
|
+
*/
|
328
|
+
async completeOAuthFlow(serverUrl, code) {
|
329
|
+
this.setState({
|
330
|
+
isCompletingOAuth: true,
|
331
|
+
authError: null,
|
332
|
+
});
|
333
|
+
try {
|
334
|
+
// If we don't have discovery data, re-discover it
|
335
|
+
if (!this.discovery) {
|
336
|
+
console.log('🔍 [OAuthHelper] Re-discovering OAuth configuration for callback');
|
337
|
+
await this.discoverOAuthConfig(serverUrl);
|
338
|
+
}
|
339
|
+
// Only re-register if we don't have a client registration for this server
|
340
|
+
if (!this.clientRegistration || this.serverUrl !== serverUrl) {
|
341
|
+
console.log('🔐 [OAuthHelper] Re-registering client for callback');
|
342
|
+
await this.registerClient(serverUrl);
|
343
|
+
}
|
344
|
+
else {
|
345
|
+
console.log('🔄 [OAuthHelper] Using existing client registration for callback');
|
346
|
+
}
|
347
|
+
const tokenResponse = await this.exchangeCodeForToken(serverUrl, code);
|
348
|
+
this.setState({
|
349
|
+
isAuthenticating: false,
|
350
|
+
isAuthenticated: true,
|
351
|
+
isCompletingOAuth: false,
|
352
|
+
authError: null,
|
353
|
+
oauthTokens: tokenResponse,
|
354
|
+
});
|
355
|
+
console.log('✅ [OAuthHelper] OAuth flow completed successfully');
|
356
|
+
return tokenResponse;
|
357
|
+
}
|
358
|
+
catch (error) {
|
359
|
+
console.error('❌ [OAuthHelper] Failed to complete OAuth flow:', error);
|
360
|
+
this.setState({
|
361
|
+
isAuthenticating: false,
|
362
|
+
isCompletingOAuth: false,
|
363
|
+
authError: error instanceof Error ? error.message : 'Failed to complete authentication',
|
364
|
+
});
|
365
|
+
throw error;
|
366
|
+
}
|
367
|
+
}
|
368
|
+
/**
|
369
|
+
* Reset authentication state
|
370
|
+
*/
|
371
|
+
resetAuth() {
|
372
|
+
this.setState({
|
373
|
+
isRequired: false,
|
374
|
+
isAuthenticated: false,
|
375
|
+
isAuthenticating: false,
|
376
|
+
isCompletingOAuth: false,
|
377
|
+
authError: null,
|
378
|
+
oauthTokens: null,
|
379
|
+
});
|
380
|
+
// Clear stored client registration
|
381
|
+
this.clientRegistration = undefined;
|
382
|
+
this.serverUrl = undefined;
|
383
|
+
try {
|
384
|
+
localStorage.removeItem(this.storageKey);
|
385
|
+
console.log('🗑️ [OAuthHelper] Cleared stored client registration');
|
386
|
+
}
|
387
|
+
catch (error) {
|
388
|
+
console.warn('⚠️ [OAuthHelper] Failed to clear client registration:', error);
|
389
|
+
}
|
390
|
+
}
|
391
|
+
/**
|
392
|
+
* Set OAuth state (internal method)
|
393
|
+
*/
|
394
|
+
setState(newState) {
|
395
|
+
this.state = { ...this.state, ...newState };
|
396
|
+
}
|
397
|
+
/**
|
398
|
+
* Generate a random state parameter for CSRF protection
|
399
|
+
*/
|
400
|
+
generateState() {
|
401
|
+
return Math.random().toString(36).substring(2, 15)
|
402
|
+
+ Math.random().toString(36).substring(2, 15);
|
403
|
+
}
|
404
|
+
}
|
405
|
+
/**
|
406
|
+
* Linear-specific OAuth configuration
|
407
|
+
*/
|
408
|
+
export const LINEAR_OAUTH_CONFIG = {
|
409
|
+
// No clientId needed - will use dynamic client registration
|
410
|
+
redirectUri: typeof window !== 'undefined' ? window.location.origin + window.location.pathname : 'http://localhost:5174',
|
411
|
+
scope: 'read write',
|
412
|
+
clientName: 'MCP Use Example',
|
413
|
+
};
|
414
|
+
/**
|
415
|
+
* Helper function to create OAuth-enabled MCP configuration
|
416
|
+
*/
|
417
|
+
export function createOAuthMCPConfig(serverUrl, accessToken) {
|
418
|
+
return {
|
419
|
+
mcpServers: {
|
420
|
+
linear: {
|
421
|
+
url: serverUrl,
|
422
|
+
authToken: accessToken,
|
423
|
+
transport: 'sse',
|
424
|
+
},
|
425
|
+
},
|
426
|
+
};
|
427
|
+
}
|