Spaces:
Running
Running
feat: implement BrowserOAuthProvider for handling OAuth flows and enhance MCPClientService with OAuth support
Browse files- src/App.tsx +62 -13
- src/config/constants.ts +2 -0
- src/services/BrowserOAuthProvider.ts +212 -0
- src/services/mcpClient.ts +60 -48
src/App.tsx
CHANGED
|
@@ -145,6 +145,21 @@ function renderMarkdown(text: string): string {
|
|
| 145 |
return DOMPurify.sanitize(marked.parse(text) as string);
|
| 146 |
}
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
const App: React.FC = () => {
|
| 149 |
const [systemPrompt, setSystemPrompt] = useState<string>(
|
| 150 |
DEFAULT_SYSTEM_PROMPT
|
|
@@ -557,7 +572,19 @@ const App: React.FC = () => {
|
|
| 557 |
|
| 558 |
let parsedResult;
|
| 559 |
try {
|
| 560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
} catch {
|
| 562 |
parsedResult = result;
|
| 563 |
}
|
|
@@ -659,13 +686,24 @@ const App: React.FC = () => {
|
|
| 659 |
const toolCallContent = extractToolCallContent(response);
|
| 660 |
|
| 661 |
if (toolCallContent) {
|
| 662 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
|
| 664 |
-
const
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
currentMessages.push(toolMessage);
|
| 670 |
setMessages([...currentMessages]);
|
| 671 |
continue;
|
|
@@ -852,13 +890,24 @@ const App: React.FC = () => {
|
|
| 852 |
const toolCallContent = extractToolCallContent(response);
|
| 853 |
|
| 854 |
if (toolCallContent) {
|
| 855 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
|
| 857 |
-
const
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
currentMessages.push(toolMessage);
|
| 863 |
setMessages([...currentMessages]);
|
| 864 |
continue;
|
|
|
|
| 145 |
return DOMPurify.sanitize(marked.parse(text) as string);
|
| 146 |
}
|
| 147 |
|
| 148 |
+
const safeStringifyToolResults = (results: any[]): string => {
|
| 149 |
+
try {
|
| 150 |
+
const stringified = JSON.stringify(results);
|
| 151 |
+
const MAX_SIZE = 5 * 1024 * 1024;
|
| 152 |
+
if (stringified.length > MAX_SIZE) {
|
| 153 |
+
console.warn(`Tool result is ${(stringified.length / 1024 / 1024).toFixed(2)}MB, truncating...`);
|
| 154 |
+
return stringified.substring(0, MAX_SIZE) + '\n\n...[TRUNCATED]';
|
| 155 |
+
}
|
| 156 |
+
return stringified;
|
| 157 |
+
} catch (error) {
|
| 158 |
+
console.error("Failed to stringify tool results:", error);
|
| 159 |
+
return JSON.stringify([{ error: "Failed to serialize tool results" }]);
|
| 160 |
+
}
|
| 161 |
+
};
|
| 162 |
+
|
| 163 |
const App: React.FC = () => {
|
| 164 |
const [systemPrompt, setSystemPrompt] = useState<string>(
|
| 165 |
DEFAULT_SYSTEM_PROMPT
|
|
|
|
| 572 |
|
| 573 |
let parsedResult;
|
| 574 |
try {
|
| 575 |
+
if (result.length > 100000) {
|
| 576 |
+
parsedResult = await new Promise((resolve) => {
|
| 577 |
+
setTimeout(() => {
|
| 578 |
+
try {
|
| 579 |
+
resolve(JSON.parse(result));
|
| 580 |
+
} catch {
|
| 581 |
+
resolve(result);
|
| 582 |
+
}
|
| 583 |
+
}, 0);
|
| 584 |
+
});
|
| 585 |
+
} else {
|
| 586 |
+
parsedResult = JSON.parse(result);
|
| 587 |
+
}
|
| 588 |
} catch {
|
| 589 |
parsedResult = result;
|
| 590 |
}
|
|
|
|
| 686 |
const toolCallContent = extractToolCallContent(response);
|
| 687 |
|
| 688 |
if (toolCallContent) {
|
| 689 |
+
currentMessages.push({
|
| 690 |
+
role: "assistant",
|
| 691 |
+
content: "_Processing tool results..._"
|
| 692 |
+
});
|
| 693 |
+
setMessages([...currentMessages]);
|
| 694 |
|
| 695 |
+
const toolResults = await executeToolCalls(toolCallContent);
|
| 696 |
+
currentMessages.pop();
|
| 697 |
+
|
| 698 |
+
const toolMessage: ToolMessage = await new Promise((resolve) => {
|
| 699 |
+
setTimeout(() => {
|
| 700 |
+
resolve({
|
| 701 |
+
role: "tool" as const,
|
| 702 |
+
content: safeStringifyToolResults(toolResults.map((r) => r.result ?? null)),
|
| 703 |
+
renderInfo: toolResults,
|
| 704 |
+
});
|
| 705 |
+
}, 0);
|
| 706 |
+
});
|
| 707 |
currentMessages.push(toolMessage);
|
| 708 |
setMessages([...currentMessages]);
|
| 709 |
continue;
|
|
|
|
| 890 |
const toolCallContent = extractToolCallContent(response);
|
| 891 |
|
| 892 |
if (toolCallContent) {
|
| 893 |
+
currentMessages.push({
|
| 894 |
+
role: "assistant",
|
| 895 |
+
content: "_Processing tool results..._"
|
| 896 |
+
});
|
| 897 |
+
setMessages([...currentMessages]);
|
| 898 |
|
| 899 |
+
const toolResults = await executeToolCalls(toolCallContent);
|
| 900 |
+
currentMessages.pop();
|
| 901 |
+
|
| 902 |
+
const toolMessage: ToolMessage = await new Promise((resolve) => {
|
| 903 |
+
setTimeout(() => {
|
| 904 |
+
resolve({
|
| 905 |
+
role: "tool" as const,
|
| 906 |
+
content: safeStringifyToolResults(toolResults.map((r) => r.result ?? null)),
|
| 907 |
+
renderInfo: toolResults,
|
| 908 |
+
});
|
| 909 |
+
}, 0);
|
| 910 |
+
});
|
| 911 |
currentMessages.push(toolMessage);
|
| 912 |
setMessages([...currentMessages]);
|
| 913 |
continue;
|
src/config/constants.ts
CHANGED
|
@@ -19,7 +19,9 @@ export const STORAGE_KEYS = {
|
|
| 19 |
OAUTH_REDIRECT_URI: "oauth_redirect_uri",
|
| 20 |
OAUTH_RESOURCE: "oauth_resource",
|
| 21 |
OAUTH_ACCESS_TOKEN: "oauth_access_token",
|
|
|
|
| 22 |
OAUTH_CODE_VERIFIER: "oauth_code_verifier",
|
|
|
|
| 23 |
OAUTH_MCP_SERVER_URL: "oauth_mcp_server_url",
|
| 24 |
OAUTH_AUTHORIZATION_SERVER_METADATA: "oauth_authorization_server_metadata",
|
| 25 |
MCP_SERVER_NAME: "mcp_server_name",
|
|
|
|
| 19 |
OAUTH_REDIRECT_URI: "oauth_redirect_uri",
|
| 20 |
OAUTH_RESOURCE: "oauth_resource",
|
| 21 |
OAUTH_ACCESS_TOKEN: "oauth_access_token",
|
| 22 |
+
OAUTH_REFRESH_TOKEN: "oauth_refresh_token",
|
| 23 |
OAUTH_CODE_VERIFIER: "oauth_code_verifier",
|
| 24 |
+
OAUTH_STATE: "oauth_state",
|
| 25 |
OAUTH_MCP_SERVER_URL: "oauth_mcp_server_url",
|
| 26 |
OAUTH_AUTHORIZATION_SERVER_METADATA: "oauth_authorization_server_metadata",
|
| 27 |
MCP_SERVER_NAME: "mcp_server_name",
|
src/services/BrowserOAuthProvider.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Browser-based OAuth Client Provider for MCP
|
| 3 |
+
*
|
| 4 |
+
* Implements the OAuthClientProvider interface from @modelcontextprotocol/sdk
|
| 5 |
+
* to handle user-facing OAuth flows in a browser environment.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import type {
|
| 9 |
+
OAuthClientMetadata,
|
| 10 |
+
OAuthClientInformationMixed,
|
| 11 |
+
OAuthTokens,
|
| 12 |
+
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
| 13 |
+
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
| 14 |
+
import { secureStorage } from "../utils/storage";
|
| 15 |
+
import { STORAGE_KEYS } from "../config/constants";
|
| 16 |
+
|
| 17 |
+
export interface BrowserOAuthProviderOptions {
|
| 18 |
+
/**
|
| 19 |
+
* The redirect URI for OAuth callbacks
|
| 20 |
+
*/
|
| 21 |
+
redirectUri: string;
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* Optional client name for metadata
|
| 25 |
+
*/
|
| 26 |
+
clientName?: string;
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Optional scopes to request
|
| 30 |
+
*/
|
| 31 |
+
scopes?: string[];
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* OAuth provider for browser-based authorization code flow with PKCE.
|
| 36 |
+
*
|
| 37 |
+
* This provider handles:
|
| 38 |
+
* - Dynamic client registration
|
| 39 |
+
* - PKCE code verifier management
|
| 40 |
+
* - Token storage in localStorage/secure storage
|
| 41 |
+
* - Browser redirects for authorization
|
| 42 |
+
*
|
| 43 |
+
* @example
|
| 44 |
+
* const provider = new BrowserOAuthProvider({
|
| 45 |
+
* redirectUri: window.location.origin + "/#/oauth/callback",
|
| 46 |
+
* clientName: "MCP WebGPU Client",
|
| 47 |
+
* scopes: ["read", "write"]
|
| 48 |
+
* });
|
| 49 |
+
*
|
| 50 |
+
* const transport = new StreamableHTTPClientTransport(serverUrl, {
|
| 51 |
+
* authProvider: provider
|
| 52 |
+
* });
|
| 53 |
+
*/
|
| 54 |
+
export class BrowserOAuthProvider implements OAuthClientProvider {
|
| 55 |
+
private _redirectUri: string;
|
| 56 |
+
private _clientName: string;
|
| 57 |
+
private _scopes: string[];
|
| 58 |
+
|
| 59 |
+
constructor(options: BrowserOAuthProviderOptions) {
|
| 60 |
+
this._redirectUri = options.redirectUri;
|
| 61 |
+
this._clientName = options.clientName || "MCP WebGPU Client";
|
| 62 |
+
this._scopes = options.scopes || [];
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
get redirectUrl(): string {
|
| 66 |
+
return this._redirectUri;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
get clientMetadata(): OAuthClientMetadata {
|
| 70 |
+
return {
|
| 71 |
+
client_name: this._clientName,
|
| 72 |
+
redirect_uris: [this._redirectUri],
|
| 73 |
+
grant_types: ["authorization_code"],
|
| 74 |
+
response_types: ["code"],
|
| 75 |
+
token_endpoint_auth_method: "none", // Public client (browser-based)
|
| 76 |
+
};
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Load saved client information from localStorage
|
| 81 |
+
*/
|
| 82 |
+
async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
|
| 83 |
+
const clientId = localStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_ID);
|
| 84 |
+
if (!clientId) {
|
| 85 |
+
return undefined;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
const clientSecret = await secureStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET);
|
| 89 |
+
|
| 90 |
+
return {
|
| 91 |
+
client_id: clientId,
|
| 92 |
+
...(clientSecret ? { client_secret: clientSecret } : {}),
|
| 93 |
+
};
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Save client information after dynamic registration
|
| 98 |
+
*/
|
| 99 |
+
async saveClientInformation(info: OAuthClientInformationMixed): Promise<void> {
|
| 100 |
+
localStorage.setItem(STORAGE_KEYS.OAUTH_CLIENT_ID, info.client_id);
|
| 101 |
+
|
| 102 |
+
if ("client_secret" in info && info.client_secret) {
|
| 103 |
+
await secureStorage.setItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET, info.client_secret);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/**
|
| 108 |
+
* Load saved OAuth tokens
|
| 109 |
+
*/
|
| 110 |
+
async tokens(): Promise<OAuthTokens | undefined> {
|
| 111 |
+
const accessToken = await secureStorage.getItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN);
|
| 112 |
+
if (!accessToken) {
|
| 113 |
+
return undefined;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
const refreshToken = await secureStorage.getItem(STORAGE_KEYS.OAUTH_REFRESH_TOKEN);
|
| 117 |
+
|
| 118 |
+
return {
|
| 119 |
+
access_token: accessToken,
|
| 120 |
+
token_type: "Bearer",
|
| 121 |
+
...(refreshToken ? { refresh_token: refreshToken } : {}),
|
| 122 |
+
};
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* Save OAuth tokens after successful authorization
|
| 127 |
+
*/
|
| 128 |
+
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
| 129 |
+
await secureStorage.setItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN, tokens.access_token);
|
| 130 |
+
|
| 131 |
+
if (tokens.refresh_token) {
|
| 132 |
+
await secureStorage.setItem(STORAGE_KEYS.OAUTH_REFRESH_TOKEN, tokens.refresh_token);
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/**
|
| 137 |
+
* Redirect browser to authorization URL
|
| 138 |
+
*/
|
| 139 |
+
redirectToAuthorization(authorizationUrl: URL): void {
|
| 140 |
+
window.location.href = authorizationUrl.toString();
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/**
|
| 144 |
+
* Save PKCE code verifier before redirect
|
| 145 |
+
*/
|
| 146 |
+
saveCodeVerifier(codeVerifier: string): void {
|
| 147 |
+
localStorage.setItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER, codeVerifier);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/**
|
| 151 |
+
* Load PKCE code verifier for token exchange
|
| 152 |
+
*/
|
| 153 |
+
codeVerifier(): string {
|
| 154 |
+
const verifier = localStorage.getItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER);
|
| 155 |
+
if (!verifier) {
|
| 156 |
+
throw new Error("No code verifier found in storage");
|
| 157 |
+
}
|
| 158 |
+
return verifier;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/**
|
| 162 |
+
* Generate OAuth state parameter for CSRF protection
|
| 163 |
+
*/
|
| 164 |
+
state(): string {
|
| 165 |
+
const state = crypto.randomUUID();
|
| 166 |
+
localStorage.setItem(STORAGE_KEYS.OAUTH_STATE, state);
|
| 167 |
+
return state;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/**
|
| 171 |
+
* Invalidate stored credentials
|
| 172 |
+
*/
|
| 173 |
+
async invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier'): Promise<void> {
|
| 174 |
+
switch (scope) {
|
| 175 |
+
case 'all':
|
| 176 |
+
localStorage.removeItem(STORAGE_KEYS.OAUTH_CLIENT_ID);
|
| 177 |
+
await secureStorage.removeItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET);
|
| 178 |
+
await secureStorage.removeItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN);
|
| 179 |
+
await secureStorage.removeItem(STORAGE_KEYS.OAUTH_REFRESH_TOKEN);
|
| 180 |
+
localStorage.removeItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER);
|
| 181 |
+
localStorage.removeItem(STORAGE_KEYS.OAUTH_STATE);
|
| 182 |
+
break;
|
| 183 |
+
case 'client':
|
| 184 |
+
localStorage.removeItem(STORAGE_KEYS.OAUTH_CLIENT_ID);
|
| 185 |
+
await secureStorage.removeItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET);
|
| 186 |
+
break;
|
| 187 |
+
case 'tokens':
|
| 188 |
+
await secureStorage.removeItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN);
|
| 189 |
+
await secureStorage.removeItem(STORAGE_KEYS.OAUTH_REFRESH_TOKEN);
|
| 190 |
+
break;
|
| 191 |
+
case 'verifier':
|
| 192 |
+
localStorage.removeItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER);
|
| 193 |
+
break;
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/**
|
| 198 |
+
* Prepare token request parameters for authorization code exchange
|
| 199 |
+
*/
|
| 200 |
+
async prepareTokenRequest(): Promise<URLSearchParams> {
|
| 201 |
+
const params = new URLSearchParams({
|
| 202 |
+
grant_type: "authorization_code",
|
| 203 |
+
redirect_uri: this._redirectUri,
|
| 204 |
+
});
|
| 205 |
+
|
| 206 |
+
if (this._scopes.length > 0) {
|
| 207 |
+
params.set("scope", this._scopes.join(" "));
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
return params;
|
| 211 |
+
}
|
| 212 |
+
}
|
src/services/mcpClient.ts
CHANGED
|
@@ -9,7 +9,8 @@ import type {
|
|
| 9 |
MCPClientState,
|
| 10 |
MCPToolResult,
|
| 11 |
} from "../types/mcp.js";
|
| 12 |
-
import { MCP_CLIENT_CONFIG, STORAGE_KEYS } from "../config/constants";
|
|
|
|
| 13 |
|
| 14 |
export class MCPClientService {
|
| 15 |
private clients: Map<string, Client> = new Map();
|
|
@@ -165,61 +166,72 @@ export class MCPClientService {
|
|
| 165 |
let transport;
|
| 166 |
const url = new URL(connection.config.url);
|
| 167 |
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
if (connection.config.auth.token) {
|
| 174 |
-
headers[
|
| 175 |
-
"Authorization"
|
| 176 |
-
] = `Bearer ${connection.config.auth.token}`;
|
| 177 |
-
}
|
| 178 |
-
break;
|
| 179 |
-
case "basic":
|
| 180 |
-
if (
|
| 181 |
-
connection.config.auth.username &&
|
| 182 |
-
connection.config.auth.password
|
| 183 |
-
) {
|
| 184 |
-
const credentials = btoa(
|
| 185 |
-
`${connection.config.auth.username}:${connection.config.auth.password}`
|
| 186 |
-
);
|
| 187 |
-
headers["Authorization"] = `Basic ${credentials}`;
|
| 188 |
-
}
|
| 189 |
-
break;
|
| 190 |
-
case "oauth":
|
| 191 |
-
if (connection.config.auth.token) {
|
| 192 |
-
headers[
|
| 193 |
-
"Authorization"
|
| 194 |
-
] = `Bearer ${connection.config.auth.token}`;
|
| 195 |
-
}
|
| 196 |
-
break;
|
| 197 |
-
}
|
| 198 |
-
}
|
| 199 |
|
| 200 |
-
|
| 201 |
-
case "streamable-http":
|
| 202 |
transport = new StreamableHTTPClientTransport(url, {
|
| 203 |
-
|
| 204 |
-
Object.keys(headers).length > 0 ? { headers } : undefined,
|
| 205 |
});
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
}
|
| 220 |
|
| 221 |
-
// Set up error handling
|
| 222 |
client.onerror = (error) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
connection.lastError = error.message;
|
| 224 |
connection.isConnected = false;
|
| 225 |
this.notifyStateChange();
|
|
|
|
| 9 |
MCPClientState,
|
| 10 |
MCPToolResult,
|
| 11 |
} from "../types/mcp.js";
|
| 12 |
+
import { MCP_CLIENT_CONFIG, STORAGE_KEYS, DEFAULTS } from "../config/constants";
|
| 13 |
+
import { BrowserOAuthProvider } from "./BrowserOAuthProvider";
|
| 14 |
|
| 15 |
export class MCPClientService {
|
| 16 |
private clients: Map<string, Client> = new Map();
|
|
|
|
| 166 |
let transport;
|
| 167 |
const url = new URL(connection.config.url);
|
| 168 |
|
| 169 |
+
if (connection.config.auth?.type === "oauth") {
|
| 170 |
+
const oauthProvider = new BrowserOAuthProvider({
|
| 171 |
+
redirectUri: window.location.origin + "/#" + DEFAULTS.OAUTH_REDIRECT_PATH,
|
| 172 |
+
clientName: MCP_CLIENT_CONFIG.NAME,
|
| 173 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
+
if (connection.config.transport === "streamable-http") {
|
|
|
|
| 176 |
transport = new StreamableHTTPClientTransport(url, {
|
| 177 |
+
authProvider: oauthProvider,
|
|
|
|
| 178 |
});
|
| 179 |
+
} else {
|
| 180 |
+
throw new Error("OAuth authentication is only supported with streamable-http transport");
|
| 181 |
+
}
|
| 182 |
+
} else {
|
| 183 |
+
const headers: Record<string, string> = {};
|
| 184 |
+
if (connection.config.auth) {
|
| 185 |
+
switch (connection.config.auth.type) {
|
| 186 |
+
case "bearer":
|
| 187 |
+
if (connection.config.auth.token) {
|
| 188 |
+
headers["Authorization"] = `Bearer ${connection.config.auth.token}`;
|
| 189 |
+
}
|
| 190 |
+
break;
|
| 191 |
+
case "basic":
|
| 192 |
+
if (
|
| 193 |
+
connection.config.auth.username &&
|
| 194 |
+
connection.config.auth.password
|
| 195 |
+
) {
|
| 196 |
+
const credentials = btoa(
|
| 197 |
+
`${connection.config.auth.username}:${connection.config.auth.password}`
|
| 198 |
+
);
|
| 199 |
+
headers["Authorization"] = `Basic ${credentials}`;
|
| 200 |
+
}
|
| 201 |
+
break;
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
|
| 205 |
+
switch (connection.config.transport) {
|
| 206 |
+
case "streamable-http":
|
| 207 |
+
transport = new StreamableHTTPClientTransport(url, {
|
| 208 |
+
requestInit:
|
| 209 |
+
Object.keys(headers).length > 0 ? { headers } : undefined,
|
| 210 |
+
});
|
| 211 |
+
break;
|
| 212 |
|
| 213 |
+
case "sse":
|
| 214 |
+
transport = new SSEClientTransport(url, {
|
| 215 |
+
requestInit:
|
| 216 |
+
Object.keys(headers).length > 0 ? { headers } : undefined,
|
| 217 |
+
});
|
| 218 |
+
break;
|
| 219 |
+
|
| 220 |
+
default:
|
| 221 |
+
throw new Error(
|
| 222 |
+
`Unsupported transport: ${connection.config.transport}`
|
| 223 |
+
);
|
| 224 |
+
}
|
| 225 |
}
|
| 226 |
|
|
|
|
| 227 |
client.onerror = (error) => {
|
| 228 |
+
if (error.message?.includes("Failed to send cancellation") &&
|
| 229 |
+
error.message?.includes("AbortError")) {
|
| 230 |
+
console.debug(`Ignoring benign cancellation error for ${serverId}:`, error.message);
|
| 231 |
+
return;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
console.error(`MCP Client error for ${serverId}:`, error);
|
| 235 |
connection.lastError = error.message;
|
| 236 |
connection.isConnected = false;
|
| 237 |
this.notifyStateChange();
|