Просмотр исходного кода

Merge pull request #5297 from Kilo-Org/mcp-auth

feat(mcp): implement oauth 2.1 authorization for http transports
John Fawcett 3 недель назад
Родитель
Сommit
320dd38e1d
59 измененных файлов с 3713 добавлено и 133 удалено
  1. 5 0
      .changeset/smart-shirts-pull.md
  2. 575 0
      apps/kilocode-docs/docs/contributing/architecture/mcp-oauth-authorization.md
  3. 23 0
      src/core/webview/webviewMessageHandler.ts
  4. 6 2
      src/i18n/locales/ar/mcp.json
  5. 6 2
      src/i18n/locales/ca/mcp.json
  6. 6 2
      src/i18n/locales/cs/mcp.json
  7. 6 2
      src/i18n/locales/de/mcp.json
  8. 6 2
      src/i18n/locales/en/mcp.json
  9. 6 2
      src/i18n/locales/es/mcp.json
  10. 6 2
      src/i18n/locales/fr/mcp.json
  11. 6 2
      src/i18n/locales/hi/mcp.json
  12. 6 2
      src/i18n/locales/id/mcp.json
  13. 6 2
      src/i18n/locales/it/mcp.json
  14. 6 2
      src/i18n/locales/ja/mcp.json
  15. 6 2
      src/i18n/locales/ko/mcp.json
  16. 6 2
      src/i18n/locales/nl/mcp.json
  17. 6 2
      src/i18n/locales/pl/mcp.json
  18. 6 2
      src/i18n/locales/pt-BR/mcp.json
  19. 6 2
      src/i18n/locales/ru/mcp.json
  20. 6 2
      src/i18n/locales/th/mcp.json
  21. 6 2
      src/i18n/locales/tr/mcp.json
  22. 6 2
      src/i18n/locales/uk/mcp.json
  23. 6 2
      src/i18n/locales/vi/mcp.json
  24. 6 2
      src/i18n/locales/zh-CN/mcp.json
  25. 6 2
      src/i18n/locales/zh-TW/mcp.json
  26. 571 5
      src/services/mcp/McpHub.ts
  27. 181 0
      src/services/mcp/oauth/McpAuthorizationDiscovery.ts
  28. 235 0
      src/services/mcp/oauth/McpOAuthBrowserFlow.ts
  29. 693 0
      src/services/mcp/oauth/McpOAuthService.ts
  30. 134 0
      src/services/mcp/oauth/McpOAuthTokenStorage.ts
  31. 224 0
      src/services/mcp/oauth/__tests__/McpOAuthTokenStorage.spec.ts
  32. 140 0
      src/services/mcp/oauth/__tests__/utils.spec.ts
  33. 5 0
      src/services/mcp/oauth/index.ts
  34. 36 0
      src/services/mcp/oauth/utils.ts
  35. 1 0
      src/shared/WebviewMessage.ts
  36. 42 0
      src/shared/mcp.ts
  37. 298 40
      webview-ui/src/components/mcp/McpView.tsx
  38. 19 2
      webview-ui/src/i18n/locales/ar/mcp.json
  39. 19 2
      webview-ui/src/i18n/locales/ca/mcp.json
  40. 19 2
      webview-ui/src/i18n/locales/cs/mcp.json
  41. 19 2
      webview-ui/src/i18n/locales/de/mcp.json
  42. 19 2
      webview-ui/src/i18n/locales/en/mcp.json
  43. 19 2
      webview-ui/src/i18n/locales/es/mcp.json
  44. 19 2
      webview-ui/src/i18n/locales/fr/mcp.json
  45. 19 2
      webview-ui/src/i18n/locales/hi/mcp.json
  46. 19 2
      webview-ui/src/i18n/locales/id/mcp.json
  47. 19 2
      webview-ui/src/i18n/locales/it/mcp.json
  48. 19 2
      webview-ui/src/i18n/locales/ja/mcp.json
  49. 19 2
      webview-ui/src/i18n/locales/ko/mcp.json
  50. 19 2
      webview-ui/src/i18n/locales/nl/mcp.json
  51. 19 2
      webview-ui/src/i18n/locales/pl/mcp.json
  52. 19 2
      webview-ui/src/i18n/locales/pt-BR/mcp.json
  53. 19 2
      webview-ui/src/i18n/locales/ru/mcp.json
  54. 19 2
      webview-ui/src/i18n/locales/th/mcp.json
  55. 19 2
      webview-ui/src/i18n/locales/tr/mcp.json
  56. 19 2
      webview-ui/src/i18n/locales/uk/mcp.json
  57. 19 2
      webview-ui/src/i18n/locales/vi/mcp.json
  58. 19 2
      webview-ui/src/i18n/locales/zh-CN/mcp.json
  59. 19 2
      webview-ui/src/i18n/locales/zh-TW/mcp.json

+ 5 - 0
.changeset/smart-shirts-pull.md

@@ -0,0 +1,5 @@
+---
+"kilo-code": patch
+---
+
+feat(mcp): implement oauth 2.1 authorization for http transports

+ 575 - 0
apps/kilocode-docs/docs/contributing/architecture/mcp-oauth-authorization.md

@@ -0,0 +1,575 @@
+---
+sidebar_position: 5
+title: "MCP OAuth Authorization"
+---
+
+# MCP OAuth Authorization
+
+### Overview
+
+Many MCP servers require authentication to access protected resources. Currently, Kilo Code only supports static credential configuration (API keys, tokens) which must be manually entered and stored. This creates friction for users and security concerns for enterprises.
+
+The MCP specification defines an OAuth 2.1-based authorization flow that enables secure, user-friendly authentication without requiring users to manually manage credentials. This document specifies how Kilo Code will implement the MCP Authorization specification to support OAuth-enabled MCP servers.
+
+### Goals
+
+1. **Eliminate manual credential management** - Users authenticate via browser-based OAuth flows instead of copying/pasting API keys
+2. **Improve security** - Tokens are obtained through secure OAuth flows with PKCE, reducing credential exposure
+3. **Support enterprise SSO** - Organizations can use their existing identity providers
+4. **Maintain compatibility** - Continue supporting static credentials for servers that don't implement OAuth
+
+### Non-Goals (MVP)
+
+- Token refresh automation (will use re-authentication flow initially)
+- Dynamic Client Registration (will rely on Client ID Metadata Documents)
+- Multiple authorization server selection (will use first available)
+
+## MCP Authorization Specification Summary
+
+The MCP Authorization spec (Protocol Revision 2025-11-25) defines an OAuth 2.1-based flow for HTTP-based MCP transports. Key components:
+
+### Roles
+
+- **MCP Server** - Acts as OAuth 2.1 Resource Server, accepts access tokens
+- **MCP Client** (Kilo Code) - Acts as OAuth 2.1 Client, obtains tokens on behalf of users
+- **Authorization Server** - Issues access tokens (may be hosted with MCP server or separate)
+
+### Discovery Flow
+
+1. Client makes unauthenticated request to MCP server
+2. Server returns `401 Unauthorized` with `WWW-Authenticate` header containing `resource_metadata` URL
+3. Client fetches Protected Resource Metadata (RFC 9728) to discover authorization server(s)
+4. Client fetches Authorization Server Metadata (RFC 8414 or OpenID Connect Discovery)
+5. Client initiates OAuth authorization flow
+
+### Client Registration
+
+The spec supports three approaches (in priority order):
+
+1. **Pre-registration** - Client has existing credentials for the server
+2. **Client ID Metadata Documents** - Client uses HTTPS URL as client_id pointing to metadata JSON
+3. **Dynamic Client Registration** - Client registers dynamically via RFC 7591
+
+### Authorization Flow
+
+1. Generate PKCE code verifier and challenge
+2. Open browser with authorization URL including `resource` parameter (RFC 8707)
+3. User authenticates and authorizes
+4. Receive authorization code via redirect
+5. Exchange code for access token
+6. Use access token in `Authorization: Bearer` header for MCP requests
+
+## System Design
+
+### Architecture Overview
+
+```
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│                           MCP OAuth Authorization Flow                           │
+├─────────────────────────────────────────────────────────────────────────────────┤
+│                                                                                  │
+│  ┌──────────────┐    1. MCP Request     ┌──────────────────┐                    │
+│  │              │ ───────────────────►  │                  │                    │
+│  │  Kilo Code   │                       │   MCP Server     │                    │
+│  │  Extension   │  ◄─────────────────── │  (Resource       │                    │
+│  │              │    2. 401 + metadata  │   Server)        │                    │
+│  └──────┬───────┘                       └──────────────────┘                    │
+│         │                                                                        │
+│         │ 3. Fetch resource metadata                                            │
+│         │ 4. Fetch auth server metadata                                         │
+│         ▼                                                                        │
+│  ┌──────────────┐                       ┌──────────────────┐                    │
+│  │   OAuth      │    5. Auth Request    │                  │                    │
+│  │   Service    │ ───────────────────►  │  Authorization   │                    │
+│  │              │                       │  Server          │                    │
+│  │  - Discovery │  ◄─────────────────── │                  │                    │
+│  │  - PKCE      │    8. Token Response  │  - User Auth     │                    │
+│  │  - Tokens    │                       │  - Consent       │                    │
+│  └──────┬───────┘                       └──────────────────┘                    │
+│         │                                        ▲                               │
+│         │ 6. Open browser                        │ 7. User authenticates        │
+│         ▼                                        │                               │
+│  ┌──────────────┐                       ┌────────┴─────────┐                    │
+│  │   Browser    │ ─────────────────────►│      User        │                    │
+│  │              │                       │                  │                    │
+│  └──────────────┘                       └──────────────────┘                    │
+│                                                                                  │
+└─────────────────────────────────────────────────────────────────────────────────┘
+```
+
+### New Components
+
+#### 1. McpOAuthService
+
+A new service responsible for managing OAuth flows for MCP servers:
+
+```typescript
+// src/services/mcp/oauth/McpOAuthService.ts
+
+interface McpOAuthService {
+	/**
+	 * Initiates OAuth flow for an MCP server that returned 401
+	 * @param serverUrl The MCP server URL
+	 * @param wwwAuthenticateHeader The WWW-Authenticate header from 401 response
+	 * @returns Promise resolving to access token
+	 */
+	initiateOAuthFlow(serverUrl: string, wwwAuthenticateHeader: string): Promise<OAuthTokens>
+
+	/**
+	 * Gets stored tokens for a server, if available and valid
+	 */
+	getStoredTokens(serverUrl: string): Promise<OAuthTokens | null>
+
+	/**
+	 * Clears stored tokens for a server (for logout/re-auth)
+	 */
+	clearTokens(serverUrl: string): Promise<void>
+
+	/**
+	 * Refreshes tokens if refresh token is available
+	 */
+	refreshTokens(serverUrl: string): Promise<OAuthTokens | null>
+}
+
+interface OAuthTokens {
+	accessToken: string
+	tokenType: string
+	expiresAt?: number
+	refreshToken?: string
+	scope?: string
+}
+```
+
+#### 2. McpAuthorizationDiscovery
+
+Handles the discovery of authorization server metadata:
+
+```typescript
+// src/services/mcp/oauth/McpAuthorizationDiscovery.ts
+
+interface McpAuthorizationDiscovery {
+	/**
+	 * Discovers authorization server from WWW-Authenticate header or well-known URIs
+	 */
+	discoverAuthorizationServer(serverUrl: string, wwwAuthenticateHeader?: string): Promise<AuthorizationServerMetadata>
+
+	/**
+	 * Fetches Protected Resource Metadata (RFC 9728)
+	 */
+	fetchResourceMetadata(metadataUrl: string): Promise<ProtectedResourceMetadata>
+
+	/**
+	 * Fetches Authorization Server Metadata (RFC 8414 / OIDC Discovery)
+	 */
+	fetchAuthServerMetadata(issuerUrl: string): Promise<AuthorizationServerMetadata>
+}
+
+interface ProtectedResourceMetadata {
+	resource: string
+	authorization_servers: string[]
+	scopes_supported?: string[]
+	// ... other RFC 9728 fields
+}
+
+interface AuthorizationServerMetadata {
+	issuer: string
+	authorization_endpoint: string
+	token_endpoint: string
+	scopes_supported?: string[]
+	response_types_supported: string[]
+	code_challenge_methods_supported?: string[]
+	client_id_metadata_document_supported?: boolean
+	registration_endpoint?: string
+	// ... other RFC 8414 fields
+}
+```
+
+#### 3. McpOAuthTokenStorage
+
+Secure storage for OAuth tokens:
+
+```typescript
+// src/services/mcp/oauth/McpOAuthTokenStorage.ts
+
+interface McpOAuthTokenStorage {
+	/**
+	 * Stores tokens securely using VS Code SecretStorage
+	 */
+	storeTokens(serverUrl: string, tokens: OAuthTokens): Promise<void>
+
+	/**
+	 * Retrieves stored tokens
+	 */
+	getTokens(serverUrl: string): Promise<OAuthTokens | null>
+
+	/**
+	 * Removes stored tokens
+	 */
+	removeTokens(serverUrl: string): Promise<void>
+
+	/**
+	 * Lists all servers with stored tokens
+	 */
+	listServers(): Promise<string[]>
+}
+```
+
+#### 4. Client ID Metadata Document Hosting
+
+For Client ID Metadata Documents, Kilo Code needs to host a metadata document. We will use static hosting on kilocode.ai:
+
+- Host at `https://kilocode.ai/.well-known/oauth-client/vscode-extension.json`
+- Simple, reliable, no runtime dependencies
+- Authorization servers can cache the document effectively
+- No attack surface from dynamic generation logic
+
+Metadata document:
+
+```json
+{
+	"client_id": "https://kilocode.ai/.well-known/oauth-client/vscode-extension.json",
+	"client_name": "Kilo Code",
+	"client_uri": "https://kilocode.ai",
+	"logo_uri": "https://kilocode.ai/logo.png",
+	"redirect_uris": ["http://127.0.0.1:0/callback", "vscode://kilocode.kilo-code/oauth/callback"],
+	"grant_types": ["authorization_code"],
+	"response_types": ["code"],
+	"token_endpoint_auth_method": "none"
+}
+```
+
+### Integration with McpHub
+
+The existing `McpHub` class needs modifications to support OAuth:
+
+```typescript
+// Modifications to McpHub.ts
+
+class McpHub {
+	private oauthService: McpOAuthService
+
+	private async connectToServer(name: string, config: ServerConfig, source: "global" | "project"): Promise<void> {
+		// ... existing connection logic ...
+
+		// For HTTP-based transports, handle OAuth
+		if (config.type === "sse" || config.type === "streamable-http") {
+			try {
+				await this.connectWithOAuth(name, config, source)
+			} catch (error) {
+				if (this.isOAuthRequired(error)) {
+					// Initiate OAuth flow
+					const tokens = await this.oauthService.initiateOAuthFlow(config.url, error.wwwAuthenticateHeader)
+					// Retry connection with token
+					await this.connectWithToken(name, config, source, tokens)
+				} else {
+					throw error
+				}
+			}
+		}
+	}
+
+	private isOAuthRequired(error: unknown): boolean {
+		// Check if error is 401 with WWW-Authenticate header
+		return error instanceof HttpError && error.status === 401 && error.headers?.["www-authenticate"]
+	}
+}
+```
+
+### Configuration Schema Updates
+
+Update the server configuration schema to support OAuth:
+
+```typescript
+// Extended server config for OAuth-enabled servers
+const OAuthServerConfigSchema = BaseConfigSchema.extend({
+	type: z.enum(["sse", "streamable-http"]),
+	url: z.string().url(),
+	headers: z.record(z.string()).optional(),
+
+	// OAuth configuration
+	oauth: z
+		.object({
+			// Override client_id if pre-registered
+			clientId: z.string().optional(),
+			clientSecret: z.string().optional(),
+
+			// Override scopes to request
+			scopes: z.array(z.string()).optional(),
+
+			// Disable OAuth for this server (use static headers instead)
+			disabled: z.boolean().optional(),
+		})
+		.optional(),
+})
+```
+
+### Browser-Based Authorization Flow
+
+The OAuth flow requires opening a browser for user authentication:
+
+```typescript
+// src/services/mcp/oauth/McpOAuthBrowserFlow.ts
+
+interface McpOAuthBrowserFlow {
+	/**
+	 * Opens browser for authorization and waits for callback
+	 */
+	authorize(params: AuthorizationParams): Promise<AuthorizationResult>
+}
+
+interface AuthorizationParams {
+	authorizationEndpoint: string
+	clientId: string
+	redirectUri: string
+	scope: string
+	state: string
+	codeChallenge: string
+	codeChallengeMethod: "S256"
+	resource: string
+}
+
+interface AuthorizationResult {
+	code: string
+	state: string
+}
+```
+
+**Redirect URI Handling:**
+
+Two approaches for receiving the OAuth callback:
+
+1. **Local HTTP Server** (Primary)
+
+    - Start temporary HTTP server on random port
+    - Use `http://127.0.0.1:{port}/callback` as redirect URI
+    - Server receives callback, extracts code, closes
+
+2. **VS Code URI Handler** (Fallback)
+    - Register `vscode://kilocode.kilo-code/oauth/callback` URI handler
+    - Works when local server isn't possible
+    - Requires VS Code to be running
+
+### Token Management
+
+#### Storage
+
+Tokens are stored using VS Code's SecretStorage API:
+
+```typescript
+// Key format: mcp-oauth-{serverUrlHash}
+const storageKey = `mcp-oauth-${hashServerUrl(serverUrl)}`
+
+// Stored value (encrypted by VS Code)
+interface StoredTokenData {
+	accessToken: string
+	refreshToken?: string
+	expiresAt?: number
+	scope?: string
+	serverUrl: string
+	issuedAt: number
+}
+```
+
+#### Token Lifecycle
+
+1. **Initial Authentication**
+
+    - User triggers connection to OAuth-enabled MCP server
+    - Server returns 401, OAuth flow initiated
+    - User authenticates in browser
+    - Tokens stored securely
+
+2. **Subsequent Connections**
+
+    - Check for stored tokens
+    - If valid, use directly
+    - If expired and refresh token available, attempt refresh
+    - If refresh fails or no refresh token, re-authenticate
+
+3. **Token Refresh** (Future Enhancement)
+    - Background refresh before expiry
+    - Automatic retry on 401 with new token
+
+### Error Handling
+
+```typescript
+// OAuth-specific errors
+class McpOAuthError extends Error {
+	constructor(
+		message: string,
+		public code: OAuthErrorCode,
+		public serverUrl: string,
+		public details?: Record<string, unknown>,
+	) {
+		super(message)
+	}
+}
+
+enum OAuthErrorCode {
+	DISCOVERY_FAILED = "discovery_failed",
+	AUTHORIZATION_FAILED = "authorization_failed",
+	TOKEN_EXCHANGE_FAILED = "token_exchange_failed",
+	TOKEN_REFRESH_FAILED = "token_refresh_failed",
+	PKCE_NOT_SUPPORTED = "pkce_not_supported",
+	USER_CANCELLED = "user_cancelled",
+	TIMEOUT = "timeout",
+}
+```
+
+### User Experience
+
+#### Connection Flow
+
+1. User adds/enables OAuth-enabled MCP server
+2. Extension detects OAuth requirement (401 response)
+3. Notification: "MCP server requires authentication. Click to sign in."
+4. User clicks → Browser opens to authorization server
+5. User authenticates and authorizes
+6. Browser redirects back → Extension receives token
+7. Connection completes → Server shows as connected
+
+#### UI Indicators
+
+- **Authenticated servers**: Show lock icon with "Authenticated" status
+- **Authentication required**: Show warning icon with "Sign in required" action
+- **Authentication expired**: Show refresh icon with "Re-authenticate" action
+
+#### Settings UI
+
+Add OAuth status to MCP server settings:
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ MCP Server: github-mcp                                      │
+├─────────────────────────────────────────────────────────────┤
+│ Status: Connected ✓                                         │
+│ Type: streamable-http                                       │
+│ URL: https://mcp.github.com                                 │
+│                                                             │
+│ Authentication                                              │
+│ ├─ Method: OAuth 2.0                                        │
+│ ├─ Status: Authenticated ✓                                  │
+│ ├─ Expires: 2024-01-15 10:30 AM                            │
+│ └─ [Sign Out] [Re-authenticate]                            │
+└─────────────────────────────────────────────────────────────┘
+```
+
+## Security Considerations
+
+### PKCE Requirement
+
+All OAuth flows MUST use PKCE with S256 challenge method:
+
+```typescript
+function generatePKCE(): { verifier: string; challenge: string } {
+	// Generate 32-byte random verifier
+	const verifier = base64UrlEncode(crypto.randomBytes(32))
+
+	// Create S256 challenge
+	const challenge = base64UrlEncode(crypto.createHash("sha256").update(verifier).digest())
+
+	return { verifier, challenge }
+}
+```
+
+### State Parameter
+
+Generate cryptographically random state to prevent CSRF:
+
+```typescript
+const state = base64UrlEncode(crypto.randomBytes(32))
+// Store state locally and verify on callback
+```
+
+### Token Storage Security
+
+- Use VS Code SecretStorage (encrypted, per-workspace)
+- Never log tokens
+- Clear tokens on extension uninstall
+- Support manual token revocation
+
+### Resource Parameter
+
+Always include `resource` parameter to bind tokens to specific MCP server:
+
+```typescript
+const authUrl = new URL(authorizationEndpoint)
+authUrl.searchParams.set("resource", mcpServerUrl)
+```
+
+### Redirect URI Validation
+
+- Only accept callbacks on registered redirect URIs
+- Validate state parameter matches
+- Use localhost with random port (not predictable)
+
+## Scope and Implementation Plan
+
+### Phase 1: Core OAuth Infrastructure
+
+- [ ] Create `McpOAuthService` with basic flow support
+- [ ] Implement `McpAuthorizationDiscovery` for metadata fetching
+- [ ] Implement `McpOAuthTokenStorage` using SecretStorage
+- [ ] Add PKCE generation utilities
+- [ ] Create local HTTP server for OAuth callbacks
+
+### Phase 2: McpHub Integration
+
+- [ ] Modify `McpHub.connectToServer()` to detect OAuth requirements
+- [ ] Add OAuth retry logic for 401 responses
+- [ ] Update server configuration schema for OAuth options
+- [ ] Add token injection to HTTP transports
+
+### Phase 3: Client ID Metadata Document
+
+- [ ] Host Kilo Code client metadata at kilocode.ai
+- [ ] Implement client_id URL generation
+- [ ] Add fallback to pre-registration for unsupported servers
+
+### Phase 4: User Experience
+
+- [ ] Add OAuth status indicators to MCP server UI
+- [ ] Implement "Sign in" / "Sign out" actions
+- [ ] Add authentication expiry notifications
+- [ ] Create re-authentication flow
+
+### Phase 5: Testing & Documentation
+
+- [ ] Unit tests for OAuth service components
+- [ ] Integration tests with mock OAuth server
+- [ ] End-to-end tests with real OAuth-enabled MCP servers
+- [ ] User documentation for OAuth-enabled servers
+
+## Future Enhancements
+
+- **Automatic token refresh** - Background refresh before expiry
+- **Dynamic Client Registration** - Support RFC 7591 for servers that require it
+- **Multiple authorization servers** - UI for selecting preferred auth server
+- **Enterprise SSO integration** - Support for organization identity providers
+- **Token sharing across workspaces** - Optional global token storage
+- **Offline token caching** - Support for offline scenarios with cached tokens
+
+## Appendix: MCP Authorization Spec Compliance Checklist
+
+### Required (MUST)
+
+- [ ] Use PKCE with S256 for all authorization requests
+- [ ] Include `resource` parameter in authorization and token requests
+- [ ] Support WWW-Authenticate header parsing for resource metadata discovery
+- [ ] Support well-known URI fallback for resource metadata
+- [ ] Support both OAuth 2.0 and OpenID Connect discovery endpoints
+- [ ] Use Authorization header with Bearer scheme for token transmission
+- [ ] Validate PKCE support before proceeding with authorization
+
+### Recommended (SHOULD)
+
+- [ ] Support Client ID Metadata Documents
+- [ ] Use scope from WWW-Authenticate header when provided
+- [ ] Fall back to scopes_supported when scope not in challenge
+- [ ] Implement step-up authorization for insufficient_scope errors
+
+### Optional (MAY)
+
+- [ ] Support Dynamic Client Registration (RFC 7591)
+- [ ] Support pre-registered client credentials
+- [ ] Implement token refresh flows

+ 23 - 0
src/core/webview/webviewMessageHandler.ts

@@ -1532,6 +1532,29 @@ export const webviewMessageHandler = async (
 			}
 			break
 		}
+		// kilocode_change start: MCP OAuth sign-in handler
+		case "mcpServerOAuthSignIn": {
+			try {
+				const mcpHub = provider.getMcpHub()
+				if (!mcpHub) {
+					provider.log("MCP Hub not available for OAuth sign-in")
+					break
+				}
+				if (!message.serverName) {
+					provider.log("Server name required for OAuth sign-in")
+					break
+				}
+				// Trigger OAuth flow for the specified server
+				await mcpHub.initiateOAuthSignIn(message.serverName, message.source as "global" | "project")
+			} catch (error) {
+				provider.log(
+					`Failed to initiate OAuth sign-in for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+				)
+				vscode.window.showErrorMessage(t("mcp:errors.oauth_signin_failed"))
+			}
+			break
+		}
+		// kilocode_change end
 		case "toggleToolAlwaysAllow": {
 			try {
 				await provider

+ 6 - 2
src/i18n/locales/ar/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "فشل في قطع الاتصال عن {{count}} خادم MCP. تحقق من المخرجات للحصول على التفاصيل.",
 		"toolNotFound": "الأداة '{{toolName}}' غير موجودة في الخادم '{{serverName}}'. الأدوات المتاحة: {{availableTools}}",
 		"serverNotFound": "خادم MCP '{{serverName}}' غير مكوّن. الخوادم المتاحة: {{availableServers}}",
-		"toolDisabled": "الأداة '{{toolName}}' في الخادم '{{serverName}}' معطلة. الأدوات المفعلة المتاحة: {{availableTools}}"
+		"toolDisabled": "الأداة '{{toolName}}' في الخادم '{{serverName}}' معطلة. الأدوات المفعلة المتاحة: {{availableTools}}",
+		"oauth_signin_failed": "فشل تسجيل الدخول OAuth. يرجى المحاولة مرة أخرى.",
+		"oauth_failed": "فشل مصادقة OAuth لـ {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "جاري إعادة تشغيل خادم MCP: {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "الخوادم قيد التحديث حالياً.",
 		"refreshing_all": "جاري تحديث كل خوادم MCP...",
 		"all_refreshed": "تم تحديث جميع خوادم MCP.",
-		"project_config_deleted": "تم حذف ملف إعدادات MCP الخاص بالمشروع. تم فصل جميع الخوادم المرتبطة بالمشروع."
+		"project_config_deleted": "تم حذف ملف إعدادات MCP الخاص بالمشروع. تم فصل جميع الخوادم المرتبطة بالمشروع.",
+		"oauth_required": "مصادقة OAuth مطلوبة للاتصال بـ '{{serverUrl}}'",
+		"oauth_success": "نجحت مصادقة OAuth لـ '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/ca/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Ha fallat la desconnexió de {{count}} servidor(s) MCP. Comprova la sortida per més detalls.",
 		"toolNotFound": "L'eina '{{toolName}}' no existeix al servidor '{{serverName}}'. Eines disponibles: {{availableTools}}",
 		"serverNotFound": "El servidor MCP '{{serverName}}' no està configurat. Servidors disponibles: {{availableServers}}",
-		"toolDisabled": "L'eina '{{toolName}}' del servidor '{{serverName}}' està desactivada. Eines activades disponibles: {{availableTools}}"
+		"toolDisabled": "L'eina '{{toolName}}' del servidor '{{serverName}}' està desactivada. Eines activades disponibles: {{availableTools}}",
+		"oauth_signin_failed": "Ha fallat l'inici de sessió OAuth. Si us plau, torna-ho a provar.",
+		"oauth_failed": "Ha fallat l'autenticació OAuth per a {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Reiniciant el servidor MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "Els servidors MCP ja s'estan actualitzant.",
 		"refreshing_all": "Actualitzant tots els servidors MCP...",
 		"all_refreshed": "Tots els servidors MCP han estat actualitzats.",
-		"project_config_deleted": "Fitxer de configuració MCP del projecte eliminat. Tots els servidors MCP del projecte han estat desconnectats."
+		"project_config_deleted": "Fitxer de configuració MCP del projecte eliminat. Tots els servidors MCP del projecte han estat desconnectats.",
+		"oauth_required": "Es requereix autenticació OAuth per connectar a '{{serverUrl}}'",
+		"oauth_success": "Autenticació OAuth correcta per a '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/cs/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Nepodařilo se odpojit {{count}} server(ů) MCP. Zkontroluj výstup pro podrobnosti.",
 		"toolNotFound": "Nástroj '{{toolName}}' neexistuje na serveru '{{serverName}}'. Dostupné nástroje: {{availableTools}}",
 		"serverNotFound": "Server MCP '{{serverName}}' není nakonfigurován. Dostupné servery: {{availableServers}}",
-		"toolDisabled": "Nástroj '{{toolName}}' na serveru '{{serverName}}' je zakázán. Dostupné povolené nástroje: {{availableTools}}"
+		"toolDisabled": "Nástroj '{{toolName}}' na serveru '{{serverName}}' je zakázán. Dostupné povolené nástroje: {{availableTools}}",
+		"oauth_signin_failed": "OAuth přihlášení selhalo. Zkuste to prosím znovu.",
+		"oauth_failed": "OAuth autentizace selhala pro {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Restartování serveru MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "Servery MCP se již obnovují.",
 		"refreshing_all": "Obnovování všech serverů MCP...",
 		"all_refreshed": "Všechny servery MCP byly obnoveny.",
-		"project_config_deleted": "Konfigurační soubor MCP projektu byl smazán. Všechny servery MCP projektu byly odpojeny."
+		"project_config_deleted": "Konfigurační soubor MCP projektu byl smazán. Všechny servery MCP projektu byly odpojeny.",
+		"oauth_required": "Pro připojení k '{{serverUrl}}' je vyžadována OAuth autentizace",
+		"oauth_success": "OAuth autentizace pro '{{serverUrl}}' byla úspěšná"
 	}
 }

+ 6 - 2
src/i18n/locales/de/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Fehler beim Trennen von {{count}} MCP-Server(n). Überprüfe die Ausgabe für Details.",
 		"toolNotFound": "Tool '{{toolName}}' existiert nicht auf Server '{{serverName}}'. Verfügbare Tools: {{availableTools}}",
 		"serverNotFound": "MCP-Server '{{serverName}}' ist nicht konfiguriert. Verfügbare Server: {{availableServers}}",
-		"toolDisabled": "Tool '{{toolName}}' auf Server '{{serverName}}' ist deaktiviert. Verfügbare aktivierte Tools: {{availableTools}}"
+		"toolDisabled": "Tool '{{toolName}}' auf Server '{{serverName}}' ist deaktiviert. Verfügbare aktivierte Tools: {{availableTools}}",
+		"oauth_signin_failed": "OAuth-Anmeldung fehlgeschlagen. Bitte versuche es erneut.",
+		"oauth_failed": "OAuth-Authentifizierung für {{serverUrl}} fehlgeschlagen: {{error}}"
 	},
 	"info": {
 		"server_restarting": "MCP-Server {{serverName}} wird neu gestartet...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "MCP-Server werden bereits aktualisiert.",
 		"refreshing_all": "Alle MCP-Server werden aktualisiert...",
 		"all_refreshed": "Alle MCP-Server wurden aktualisiert.",
-		"project_config_deleted": "Projekt-MCP-Konfigurationsdatei gelöscht. Alle Projekt-MCP-Server wurden getrennt."
+		"project_config_deleted": "Projekt-MCP-Konfigurationsdatei gelöscht. Alle Projekt-MCP-Server wurden getrennt.",
+		"oauth_required": "OAuth-Authentifizierung erforderlich, um mit '{{serverUrl}}' zu verbinden",
+		"oauth_success": "OAuth-Authentifizierung für '{{serverUrl}}' erfolgreich"
 	}
 }

+ 6 - 2
src/i18n/locales/en/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Failed to disconnect {{count}} MCP server(s). Check the output for details.",
 		"toolNotFound": "Tool '{{toolName}}' does not exist on server '{{serverName}}'. Available tools: {{availableTools}}",
 		"serverNotFound": "MCP server '{{serverName}}' is not configured. Available servers: {{availableServers}}",
-		"toolDisabled": "Tool '{{toolName}}' on server '{{serverName}}' is disabled. Available enabled tools: {{availableTools}}"
+		"toolDisabled": "Tool '{{toolName}}' on server '{{serverName}}' is disabled. Available enabled tools: {{availableTools}}",
+		"oauth_signin_failed": "OAuth sign-in failed. Please try again.",
+		"oauth_failed": "OAuth authentication failed for {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Restarting {{serverName}} MCP server...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "MCP servers are already refreshing.",
 		"refreshing_all": "Refreshing all MCP servers...",
 		"all_refreshed": "All MCP servers have been refreshed.",
-		"project_config_deleted": "Project MCP configuration file deleted. All project MCP servers have been disconnected."
+		"project_config_deleted": "Project MCP configuration file deleted. All project MCP servers have been disconnected.",
+		"oauth_required": "MCP server requires authentication: {{serverUrl}}",
+		"oauth_success": "Successfully authenticated with: {{serverUrl}}"
 	}
 }

+ 6 - 2
src/i18n/locales/es/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Error al desconectar {{count}} servidor(es) MCP. Revisa la salida para más detalles.",
 		"toolNotFound": "La herramienta '{{toolName}}' no existe en el servidor '{{serverName}}'. Herramientas disponibles: {{availableTools}}",
 		"serverNotFound": "El servidor MCP '{{serverName}}' no está configurado. Servidores disponibles: {{availableServers}}",
-		"toolDisabled": "La herramienta '{{toolName}}' del servidor '{{serverName}}' está desactivada. Herramientas activadas disponibles: {{availableTools}}"
+		"toolDisabled": "La herramienta '{{toolName}}' del servidor '{{serverName}}' está desactivada. Herramientas activadas disponibles: {{availableTools}}",
+		"oauth_signin_failed": "El inicio de sesión OAuth falló. Por favor, inténtalo de nuevo.",
+		"oauth_failed": "La autenticación OAuth falló para {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Reiniciando el servidor MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "Los servidores MCP ya se están actualizando.",
 		"refreshing_all": "Actualizando todos los servidores MCP...",
 		"all_refreshed": "Todos los servidores MCP han sido actualizados.",
-		"project_config_deleted": "Archivo de configuración MCP del proyecto eliminado. Todos los servidores MCP del proyecto han sido desconectados."
+		"project_config_deleted": "Archivo de configuración MCP del proyecto eliminado. Todos los servidores MCP del proyecto han sido desconectados.",
+		"oauth_required": "Se requiere autenticación OAuth para conectar con '{{serverUrl}}'",
+		"oauth_success": "Autenticación OAuth exitosa para '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/fr/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Échec de la déconnexion de {{count}} serveur(s) MCP. Vérifiez la sortie pour plus de détails.",
 		"toolNotFound": "L'outil '{{toolName}}' n'existe pas sur le serveur '{{serverName}}'. Outils disponibles : {{availableTools}}",
 		"serverNotFound": "Le serveur MCP '{{serverName}}' n'est pas configuré. Serveurs disponibles : {{availableServers}}",
-		"toolDisabled": "L'outil '{{toolName}}' sur le serveur '{{serverName}}' est désactivé. Outils activés disponibles : {{availableTools}}"
+		"toolDisabled": "L'outil '{{toolName}}' sur le serveur '{{serverName}}' est désactivé. Outils activés disponibles : {{availableTools}}",
+		"oauth_signin_failed": "Échec de la connexion OAuth. Veuillez réessayer.",
+		"oauth_failed": "Échec de l'authentification OAuth pour {{serverUrl}} : {{error}}"
 	},
 	"info": {
 		"server_restarting": "Redémarrage du serveur MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "Les serveurs MCP sont déjà en cours de rafraîchissement.",
 		"refreshing_all": "Rafraîchissement de tous les serveurs MCP...",
 		"all_refreshed": "Tous les serveurs MCP ont été rafraîchis.",
-		"project_config_deleted": "Fichier de configuration MCP du projet supprimé. Tous les serveurs MCP du projet ont été déconnectés."
+		"project_config_deleted": "Fichier de configuration MCP du projet supprimé. Tous les serveurs MCP du projet ont été déconnectés.",
+		"oauth_required": "Authentification OAuth requise pour se connecter à '{{serverUrl}}'",
+		"oauth_success": "Authentification OAuth réussie pour '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/hi/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "{{count}} MCP सर्वर डिस्कनेक्ट करने में विफल। विवरण के लिए आउटपुट देखें।",
 		"toolNotFound": "टूल '{{toolName}}' सर्वर '{{serverName}}' पर मौजूद नहीं है। उपलब्ध टूल: {{availableTools}}",
 		"serverNotFound": "MCP सर्वर '{{serverName}}' कॉन्फ़िगर नहीं है। उपलब्ध सर्वर: {{availableServers}}",
-		"toolDisabled": "सर्वर '{{serverName}}' पर टूल '{{toolName}}' अक्षम है। उपलब्ध सक्षम टूल: {{availableTools}}"
+		"toolDisabled": "सर्वर '{{serverName}}' पर टूल '{{toolName}}' अक्षम है। उपलब्ध सक्षम टूल: {{availableTools}}",
+		"oauth_signin_failed": "OAuth साइन-इन विफल। कृपया पुनः प्रयास करें।",
+		"oauth_failed": "{{serverUrl}} के लिए OAuth प्रमाणीकरण विफल: {{error}}"
 	},
 	"info": {
 		"server_restarting": "{{serverName}} MCP सर्वर पुनः प्रारंभ हो रहा है...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "एमसीपी सर्वर पहले से ही रीफ्रेश हो रहे हैं।",
 		"refreshing_all": "सभी एमसीपी सर्वर रीफ्रेश हो रहे हैं...",
 		"all_refreshed": "सभी एमसीपी सर्वर रीफ्रेश हो गए हैं।",
-		"project_config_deleted": "प्रोजेक्ट एमसीपी कॉन्फ़िगरेशन फ़ाइल हटा दी गई है। सभी प्रोजेक्ट एमसीपी सर्वर डिस्कनेक्ट कर दिए गए हैं।"
+		"project_config_deleted": "प्रोजेक्ट एमसीपी कॉन्फ़िगरेशन फ़ाइल हटा दी गई है। सभी प्रोजेक्ट एमसीपी सर्वर डिस्कनेक्ट कर दिए गए हैं।",
+		"oauth_required": "'{{serverUrl}}' से कनेक्ट करने के लिए OAuth प्रमाणीकरण आवश्यक है",
+		"oauth_success": "'{{serverUrl}}' के लिए OAuth प्रमाणीकरण सफल"
 	}
 }

+ 6 - 2
src/i18n/locales/id/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Gagal memutus koneksi {{count}} server MCP. Periksa output untuk detailnya.",
 		"toolNotFound": "Tool '{{toolName}}' tidak ada di server '{{serverName}}'. Tool yang tersedia: {{availableTools}}",
 		"serverNotFound": "Server MCP '{{serverName}}' tidak dikonfigurasi. Server yang tersedia: {{availableServers}}",
-		"toolDisabled": "Tool '{{toolName}}' di server '{{serverName}}' dinonaktifkan. Tool aktif yang tersedia: {{availableTools}}"
+		"toolDisabled": "Tool '{{toolName}}' di server '{{serverName}}' dinonaktifkan. Tool aktif yang tersedia: {{availableTools}}",
+		"oauth_signin_failed": "OAuth sign-in gagal. Silakan coba lagi.",
+		"oauth_failed": "Autentikasi OAuth gagal untuk {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Merestart server MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "Server MCP sudah sedang di-refresh.",
 		"refreshing_all": "Me-refresh semua server MCP...",
 		"all_refreshed": "Semua server MCP telah di-refresh.",
-		"project_config_deleted": "File konfigurasi MCP proyek dihapus. Semua server MCP proyek telah diputus koneksinya."
+		"project_config_deleted": "File konfigurasi MCP proyek dihapus. Semua server MCP proyek telah diputus koneksinya.",
+		"oauth_required": "Autentikasi OAuth diperlukan untuk terhubung ke '{{serverUrl}}'",
+		"oauth_success": "Autentikasi OAuth berhasil untuk '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/it/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Impossibile disconnettere {{count}} server MCP. Controlla l'output per i dettagli.",
 		"toolNotFound": "Lo strumento '{{toolName}}' non esiste sul server '{{serverName}}'. Strumenti disponibili: {{availableTools}}",
 		"serverNotFound": "Il server MCP '{{serverName}}' non è configurato. Server disponibili: {{availableServers}}",
-		"toolDisabled": "Lo strumento '{{toolName}}' sul server '{{serverName}}' è disabilitato. Strumenti abilitati disponibili: {{availableTools}}"
+		"toolDisabled": "Lo strumento '{{toolName}}' sul server '{{serverName}}' è disabilitato. Strumenti abilitati disponibili: {{availableTools}}",
+		"oauth_signin_failed": "Accesso OAuth non riuscito. Riprova.",
+		"oauth_failed": "Autenticazione OAuth non riuscita per {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Riavvio del server MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "I server MCP sono già in aggiornamento.",
 		"refreshing_all": "Aggiornamento di tutti i server MCP...",
 		"all_refreshed": "Tutti i server MCP sono stati aggiornati.",
-		"project_config_deleted": "File di configurazione MCP del progetto eliminato. Tutti i server MCP del progetto sono stati disconnessi."
+		"project_config_deleted": "File di configurazione MCP del progetto eliminato. Tutti i server MCP del progetto sono stati disconnessi.",
+		"oauth_required": "Autenticazione OAuth richiesta per connettersi a '{{serverUrl}}'",
+		"oauth_success": "Autenticazione OAuth riuscita per '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/ja/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "{{count}}個のMCPサーバーの切断に失敗しました。詳細は出力を確認してください。",
 		"toolNotFound": "ツール '{{toolName}}' はサーバー '{{serverName}}' に存在しません。利用可能なツール: {{availableTools}}",
 		"serverNotFound": "MCPサーバー '{{serverName}}' は設定されていません。利用可能なサーバー: {{availableServers}}",
-		"toolDisabled": "サーバー '{{serverName}}' のツール '{{toolName}}' は無効です。利用可能な有効なツール: {{availableTools}}"
+		"toolDisabled": "サーバー '{{serverName}}' のツール '{{toolName}}' は無効です。利用可能な有効なツール: {{availableTools}}",
+		"oauth_signin_failed": "OAuthサインインに失敗しました。もう一度お試しください。",
+		"oauth_failed": "{{serverUrl}}のOAuth認証に失敗しました: {{error}}"
 	},
 	"info": {
 		"server_restarting": "MCPサーバー{{serverName}}を再起動中...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "MCPサーバーはすでに更新中です。",
 		"refreshing_all": "すべてのMCPサーバーを更新しています...",
 		"all_refreshed": "すべてのMCPサーバーが更新されました。",
-		"project_config_deleted": "プロジェクトMCP設定ファイルが削除されました。すべてのプロジェクトMCPサーバーが切断されました。"
+		"project_config_deleted": "プロジェクトMCP設定ファイルが削除されました。すべてのプロジェクトMCPサーバーが切断されました。",
+		"oauth_required": "'{{serverUrl}}'への接続にはOAuth認証が必要です",
+		"oauth_success": "'{{serverUrl}}'のOAuth認証に成功しました"
 	}
 }

+ 6 - 2
src/i18n/locales/ko/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "{{count}}개의 MCP 서버 연결 해제 실패. 자세한 내용은 출력을 확인하세요.",
 		"toolNotFound": "도구 '{{toolName}}'이(가) 서버 '{{serverName}}'에 존재하지 않습니다. 사용 가능한 도구: {{availableTools}}",
 		"serverNotFound": "MCP 서버 '{{serverName}}'이(가) 구성되지 않았습니다. 사용 가능한 서버: {{availableServers}}",
-		"toolDisabled": "서버 '{{serverName}}'의 도구 '{{toolName}}'이(가) 비활성화되었습니다. 사용 가능한 활성화된 도구: {{availableTools}}"
+		"toolDisabled": "서버 '{{serverName}}'의 도구 '{{toolName}}'이(가) 비활성화되었습니다. 사용 가능한 활성화된 도구: {{availableTools}}",
+		"oauth_signin_failed": "OAuth 로그인에 실패했습니다. 다시 시도해 주세요.",
+		"oauth_failed": "{{serverUrl}}의 OAuth 인증 실패: {{error}}"
 	},
 	"info": {
 		"server_restarting": "{{serverName}} MCP 서버를 재시작하는 중...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "MCP 서버가 이미 새로 고쳐지고 있습니다.",
 		"refreshing_all": "모든 MCP 서버를 새로 고치는 중...",
 		"all_refreshed": "모든 MCP 서버가 새로 고쳐졌습니다.",
-		"project_config_deleted": "프로젝트 MCP 구성 파일이 삭제되었습니다. 모든 프로젝트 MCP 서버가 연결 해제되었습니다."
+		"project_config_deleted": "프로젝트 MCP 구성 파일이 삭제되었습니다. 모든 프로젝트 MCP 서버가 연결 해제되었습니다.",
+		"oauth_required": "'{{serverUrl}}'에 연결하려면 OAuth 인증이 필요해요",
+		"oauth_success": "'{{serverUrl}}' OAuth 인증 성공"
 	}
 }

+ 6 - 2
src/i18n/locales/nl/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Loskoppelen van {{count}} MCP-server(s) mislukt. Controleer de uitvoer voor details.",
 		"toolNotFound": "Tool '{{toolName}}' bestaat niet op server '{{serverName}}'. Beschikbare tools: {{availableTools}}",
 		"serverNotFound": "MCP-server '{{serverName}}' is niet geconfigureerd. Beschikbare servers: {{availableServers}}",
-		"toolDisabled": "Tool '{{toolName}}' op server '{{serverName}}' is uitgeschakeld. Beschikbare ingeschakelde tools: {{availableTools}}"
+		"toolDisabled": "Tool '{{toolName}}' op server '{{serverName}}' is uitgeschakeld. Beschikbare ingeschakelde tools: {{availableTools}}",
+		"oauth_signin_failed": "OAuth-aanmelding mislukt. Probeer het opnieuw.",
+		"oauth_failed": "OAuth-authenticatie mislukt voor {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "{{serverName}} MCP-server wordt opnieuw gestart...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "MCP-servers worden al vernieuwd.",
 		"refreshing_all": "Alle MCP-servers worden vernieuwd...",
 		"all_refreshed": "Alle MCP-servers zijn vernieuwd.",
-		"project_config_deleted": "Project MCP-configuratiebestand verwijderd. Alle project MCP-servers zijn losgekoppeld."
+		"project_config_deleted": "Project MCP-configuratiebestand verwijderd. Alle project MCP-servers zijn losgekoppeld.",
+		"oauth_required": "OAuth-authenticatie vereist om verbinding te maken met '{{serverUrl}}'",
+		"oauth_success": "OAuth-authenticatie geslaagd voor '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/pl/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Nie udało się odłączyć {{count}} serwera(ów) MCP. Sprawdź dane wyjściowe, aby uzyskać szczegóły.",
 		"toolNotFound": "Narzędzie '{{toolName}}' nie istnieje na serwerze '{{serverName}}'. Dostępne narzędzia: {{availableTools}}",
 		"serverNotFound": "Serwer MCP '{{serverName}}' nie jest skonfigurowany. Dostępne serwery: {{availableServers}}",
-		"toolDisabled": "Narzędzie '{{toolName}}' na serwerze '{{serverName}}' jest wyłączone. Dostępne włączone narzędzia: {{availableTools}}"
+		"toolDisabled": "Narzędzie '{{toolName}}' na serwerze '{{serverName}}' jest wyłączone. Dostępne włączone narzędzia: {{availableTools}}",
+		"oauth_signin_failed": "Logowanie OAuth nie powiodło się. Spróbuj ponownie.",
+		"oauth_failed": "Uwierzytelnianie OAuth nie powiodło się dla {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Ponowne uruchamianie serwera MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "Serwery MCP są już odświeżane.",
 		"refreshing_all": "Odświeżanie wszystkich serwerów MCP...",
 		"all_refreshed": "Wszystkie serwery MCP zostały odświeżone.",
-		"project_config_deleted": "Plik konfiguracyjny MCP projektu został usunięty. Wszystkie serwery MCP projektu zostały odłączone."
+		"project_config_deleted": "Plik konfiguracyjny MCP projektu został usunięty. Wszystkie serwery MCP projektu zostały odłączone.",
+		"oauth_required": "Wymagane uwierzytelnianie OAuth do połączenia z '{{serverUrl}}'",
+		"oauth_success": "Uwierzytelnianie OAuth powiodło się dla '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/pt-BR/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Falha ao desconectar {{count}} servidor(es) MCP. Verifique a saída para detalhes.",
 		"toolNotFound": "A ferramenta '{{toolName}}' não existe no servidor '{{serverName}}'. Ferramentas disponíveis: {{availableTools}}",
 		"serverNotFound": "O servidor MCP '{{serverName}}' não está configurado. Servidores disponíveis: {{availableServers}}",
-		"toolDisabled": "A ferramenta '{{toolName}}' no servidor '{{serverName}}' está desabilitada. Ferramentas habilitadas disponíveis: {{availableTools}}"
+		"toolDisabled": "A ferramenta '{{toolName}}' no servidor '{{serverName}}' está desabilitada. Ferramentas habilitadas disponíveis: {{availableTools}}",
+		"oauth_signin_failed": "Falha no login OAuth. Por favor, tente novamente.",
+		"oauth_failed": "Falha na autenticação OAuth para {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Reiniciando o servidor MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "Os servidores MCP já estão atualizando.",
 		"refreshing_all": "Atualizando todos os servidores MCP...",
 		"all_refreshed": "Todos os servidores MCP foram atualizados.",
-		"project_config_deleted": "Arquivo de configuração MCP do projeto excluído. Todos os servidores MCP do projeto foram desconectados."
+		"project_config_deleted": "Arquivo de configuração MCP do projeto excluído. Todos os servidores MCP do projeto foram desconectados.",
+		"oauth_required": "Autenticação OAuth necessária para conectar a '{{serverUrl}}'",
+		"oauth_success": "Autenticação OAuth bem-sucedida para '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/ru/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Не удалось отключить {{count}} MCP сервер(ов). Проверьте вывод для получения подробностей.",
 		"toolNotFound": "Инструмент '{{toolName}}' не существует на сервере '{{serverName}}'. Доступные инструменты: {{availableTools}}",
 		"serverNotFound": "MCP сервер '{{serverName}}' не настроен. Доступные серверы: {{availableServers}}",
-		"toolDisabled": "Инструмент '{{toolName}}' на сервере '{{serverName}}' отключен. Доступные включенные инструменты: {{availableTools}}"
+		"toolDisabled": "Инструмент '{{toolName}}' на сервере '{{serverName}}' отключен. Доступные включенные инструменты: {{availableTools}}",
+		"oauth_signin_failed": "Ошибка входа через OAuth. Пожалуйста, попробуйте снова.",
+		"oauth_failed": "Ошибка OAuth-аутентификации для {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Перезапуск сервера MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "MCP серверы уже обновляются.",
 		"refreshing_all": "Обновление всех MCP серверов...",
 		"all_refreshed": "Все MCP серверы обновлены.",
-		"project_config_deleted": "Файл конфигурации MCP проекта удален. Все MCP серверы проекта отключены."
+		"project_config_deleted": "Файл конфигурации MCP проекта удален. Все MCP серверы проекта отключены.",
+		"oauth_required": "Требуется OAuth-аутентификация для подключения к '{{serverUrl}}'",
+		"oauth_success": "OAuth-аутентификация успешно выполнена для '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/th/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "ล้มเหลวในการตัดการเชื่อมต่อเซิร์ฟเวอร์ MCP {{count}} ตัว ตรวจสอบผลลัพธ์สำหรับรายละเอียด",
 		"toolNotFound": "เครื่องมือ '{{toolName}}' ไม่มีอยู่บนเซิร์ฟเวอร์ '{{serverName}}' เครื่องมือที่ใช้ได้: {{availableTools}}",
 		"serverNotFound": "เซิร์ฟเวอร์ MCP '{{serverName}}' ไม่ได้รับการกำหนดค่า เซิร์ฟเวอร์ที่ใช้ได้: {{availableServers}}",
-		"toolDisabled": "เครื่องมือ '{{toolName}}' บนเซิร์ฟเวอร์ '{{serverName}}' ถูกปิดใช้งาน เครื่องมือที่เปิดใช้งานที่ใช้ได้: {{availableTools}}"
+		"toolDisabled": "เครื่องมือ '{{toolName}}' บนเซิร์ฟเวอร์ '{{serverName}}' ถูกปิดใช้งาน เครื่องมือที่เปิดใช้งานที่ใช้ได้: {{availableTools}}",
+		"oauth_signin_failed": "การลงชื่อเข้าใช้ OAuth ล้มเหลว โปรดลองอีกครั้ง",
+		"oauth_failed": "การยืนยันตัวตน OAuth ล้มเหลวสำหรับ {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "กำลังรีสตาร์ทเซิร์ฟเวอร์ MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "เซิร์ฟเวอร์ MCP กำลังรีเฟรชอยู่แล้ว",
 		"refreshing_all": "กำลังรีเฟรชเซิร์ฟเวอร์ MCP ทั้งหมด...",
 		"all_refreshed": "เซิร์ฟเวอร์ MCP ทั้งหมดได้รับการรีเฟรชแล้ว",
-		"project_config_deleted": "ลบไฟล์กำหนดค่า MCP ของโปรเจ็กต์แล้ว เซิร์ฟเวอร์ MCP ของโปรเจ็กต์ทั้งหมดถูกตัดการเชื่อมต่อ"
+		"project_config_deleted": "ลบไฟล์กำหนดค่า MCP ของโปรเจ็กต์แล้ว เซิร์ฟเวอร์ MCP ของโปรเจ็กต์ทั้งหมดถูกตัดการเชื่อมต่อ",
+		"oauth_required": "ต้องการยืนยันตัวตน OAuth เพื่อเชื่อมต่อกับ '{{serverUrl}}'",
+		"oauth_success": "ยืนยันตัวตน OAuth สำเร็จสำหรับ '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/tr/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "{{count}} MCP sunucusu bağlantısı kesilemedi. Ayrıntılar için çıktıyı kontrol edin.",
 		"toolNotFound": "Araç '{{toolName}}' sunucu '{{serverName}}' üzerinde mevcut değil. Mevcut araçlar: {{availableTools}}",
 		"serverNotFound": "MCP sunucusu '{{serverName}}' yapılandırılmamış. Mevcut sunucular: {{availableServers}}",
-		"toolDisabled": "Sunucu '{{serverName}}' üzerindeki araç '{{toolName}}' devre dışı. Mevcut etkin araçlar: {{availableTools}}"
+		"toolDisabled": "Sunucu '{{serverName}}' üzerindeki araç '{{toolName}}' devre dışı. Mevcut etkin araçlar: {{availableTools}}",
+		"oauth_signin_failed": "OAuth oturum açma başarısız. Lütfen tekrar deneyin.",
+		"oauth_failed": "{{serverUrl}} için OAuth kimlik doğrulaması başarısız: {{error}}"
 	},
 	"info": {
 		"server_restarting": "{{serverName}} MCP sunucusu yeniden başlatılıyor...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "MCP sunucuları zaten yenileniyor.",
 		"refreshing_all": "Tüm MCP sunucuları yenileniyor...",
 		"all_refreshed": "Tüm MCP sunucuları yenilendi.",
-		"project_config_deleted": "Proje MCP yapılandırma dosyası silindi. Tüm proje MCP sunucuları bağlantısı kesildi."
+		"project_config_deleted": "Proje MCP yapılandırma dosyası silindi. Tüm proje MCP sunucuları bağlantısı kesildi.",
+		"oauth_required": "'{{serverUrl}}' sunucusuna bağlanmak için OAuth kimlik doğrulaması gerekli",
+		"oauth_success": "'{{serverUrl}}' için OAuth kimlik doğrulaması başarılı"
 	}
 }

+ 6 - 2
src/i18n/locales/uk/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Не вдалося відключити {{count}} сервер(ів) MCP. Перевір вивід для деталей.",
 		"toolNotFound": "Інструмент '{{toolName}}' не існує на сервері '{{serverName}}'. Доступні інструменти: {{availableTools}}",
 		"serverNotFound": "Сервер MCP '{{serverName}}' не налаштований. Доступні сервери: {{availableServers}}",
-		"toolDisabled": "Інструмент '{{toolName}}' на сервері '{{serverName}}' вимкнено. Доступні увімкнені інструменти: {{availableTools}}"
+		"toolDisabled": "Інструмент '{{toolName}}' на сервері '{{serverName}}' вимкнено. Доступні увімкнені інструменти: {{availableTools}}",
+		"oauth_signin_failed": "Помилка входу OAuth. Будь ласка, спробуйте ще раз.",
+		"oauth_failed": "Помилка автентифікації OAuth для {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Перезапуск сервера MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "Сервери MCP вже оновлюються.",
 		"refreshing_all": "Оновлення всіх серверів MCP...",
 		"all_refreshed": "Усі сервери MCP оновлено.",
-		"project_config_deleted": "Файл конфігурації MCP проекту видалено. Усі сервери MCP проекту відключено."
+		"project_config_deleted": "Файл конфігурації MCP проекту видалено. Усі сервери MCP проекту відключено.",
+		"oauth_required": "Для підключення до '{{serverUrl}}' потрібна автентифікація OAuth",
+		"oauth_success": "Автентифікація OAuth для '{{serverUrl}}' успішна"
 	}
 }

+ 6 - 2
src/i18n/locales/vi/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "Không thể ngắt kết nối {{count}} máy chủ MCP. Kiểm tra đầu ra để biết chi tiết.",
 		"toolNotFound": "Công cụ '{{toolName}}' không tồn tại trên máy chủ '{{serverName}}'. Công cụ có sẵn: {{availableTools}}",
 		"serverNotFound": "Máy chủ MCP '{{serverName}}' chưa được cấu hình. Máy chủ có sẵn: {{availableServers}}",
-		"toolDisabled": "Công cụ '{{toolName}}' trên máy chủ '{{serverName}}' đã bị vô hiệu hóa. Công cụ đã kích hoạt có sẵn: {{availableTools}}"
+		"toolDisabled": "Công cụ '{{toolName}}' trên máy chủ '{{serverName}}' đã bị vô hiệu hóa. Công cụ đã kích hoạt có sẵn: {{availableTools}}",
+		"oauth_signin_failed": "Đăng nhập OAuth thất bại. Vui lòng thử lại.",
+		"oauth_failed": "Xác thực OAuth thất bại cho {{serverUrl}}: {{error}}"
 	},
 	"info": {
 		"server_restarting": "Đang khởi động lại máy chủ MCP {{serverName}}...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "Các máy chủ MCP đã đang làm mới.",
 		"refreshing_all": "Đang làm mới tất cả các máy chủ MCP...",
 		"all_refreshed": "Tất cả các máy chủ MCP đã được làm mới.",
-		"project_config_deleted": "Tệp cấu hình MCP của dự án đã bị xóa. Tất cả các máy chủ MCP của dự án đã bị ngắt kết nối."
+		"project_config_deleted": "Tệp cấu hình MCP của dự án đã bị xóa. Tất cả các máy chủ MCP của dự án đã bị ngắt kết nối.",
+		"oauth_required": "Cần xác thực OAuth để kết nối với '{{serverUrl}}'",
+		"oauth_success": "Xác thực OAuth thành công cho '{{serverUrl}}'"
 	}
 }

+ 6 - 2
src/i18n/locales/zh-CN/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "断开 {{count}} 个 MCP 服务器失败。请查看输出了解详情。",
 		"toolNotFound": "工具 '{{toolName}}' 在服务器 '{{serverName}}' 上不存在。可用工具: {{availableTools}}",
 		"serverNotFound": "MCP 服务器 '{{serverName}}' 未配置。可用服务器: {{availableServers}}",
-		"toolDisabled": "服务器 '{{serverName}}' 上的工具 '{{toolName}}' 已禁用。可用的已启用工具: {{availableTools}}"
+		"toolDisabled": "服务器 '{{serverName}}' 上的工具 '{{toolName}}' 已禁用。可用的已启用工具: {{availableTools}}",
+		"oauth_signin_failed": "OAuth 登录失败。请重试。",
+		"oauth_failed": "{{serverUrl}} 的 OAuth 认证失败: {{error}}"
 	},
 	"info": {
 		"server_restarting": "正在重启{{serverName}}MCP服务器...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "MCP 服务器已在刷新中。",
 		"refreshing_all": "正在刷新所有 MCP 服务器...",
 		"all_refreshed": "所有 MCP 服务器已刷新。",
-		"project_config_deleted": "项目MCP配置文件已删除。所有项目MCP服务器已断开连接。"
+		"project_config_deleted": "项目MCP配置文件已删除。所有项目MCP服务器已断开连接。",
+		"oauth_required": "连接 '{{serverUrl}}' 需要 OAuth 认证",
+		"oauth_success": "'{{serverUrl}}' 的 OAuth 认证成功"
 	}
 }

+ 6 - 2
src/i18n/locales/zh-TW/mcp.json

@@ -11,7 +11,9 @@
 		"disconnect_servers_partial": "斷開 {{count}} 個 MCP 伺服器失敗。請查看輸出了解詳情。",
 		"toolNotFound": "工具 '{{toolName}}' 在伺服器 '{{serverName}}' 上不存在。可用工具: {{availableTools}}",
 		"serverNotFound": "MCP 伺服器 '{{serverName}}' 未設定。可用伺服器: {{availableServers}}",
-		"toolDisabled": "伺服器 '{{serverName}}' 上的工具 '{{toolName}}' 已停用。可用的已啟用工具: {{availableTools}}"
+		"toolDisabled": "伺服器 '{{serverName}}' 上的工具 '{{toolName}}' 已停用。可用的已啟用工具: {{availableTools}}",
+		"oauth_signin_failed": "OAuth 登入失敗。請重試。",
+		"oauth_failed": "OAuth 驗證 {{serverUrl}} 失敗:{{error}}"
 	},
 	"info": {
 		"server_restarting": "正在重啟{{serverName}}MCP 伺服器...",
@@ -23,6 +25,8 @@
 		"already_refreshing": "MCP 伺服器已在重新整理中。",
 		"refreshing_all": "正在重新整理所有 MCP 伺服器...",
 		"all_refreshed": "所有 MCP 伺服器已重新整理。",
-		"project_config_deleted": "專案 MCP 設定檔案已刪除。所有專案 MCP 伺服器已斷開連接。"
+		"project_config_deleted": "專案 MCP 設定檔案已刪除。所有專案 MCP 伺服器已斷開連接。",
+		"oauth_required": "需要進行 OAuth 驗證才能連線至「{{serverUrl}}」",
+		"oauth_success": "OAuth 驗證「{{serverUrl}}」成功"
 	}
 }

+ 571 - 5
src/services/mcp/McpHub.ts

@@ -22,6 +22,8 @@ import { t } from "../../i18n"
 import { ClineProvider } from "../../core/webview/ClineProvider"
 import { GlobalFileNames } from "../../shared/globalFileNames"
 import {
+	McpAuthDebugInfo,
+	McpAuthStatus,
 	McpResource,
 	McpResourceResponse,
 	McpResourceTemplate,
@@ -35,7 +37,9 @@ import { injectVariables } from "../../utils/config"
 import { NotificationService } from "./kilocode/NotificationService"
 import { safeWriteJson } from "../../utils/safeWriteJson"
 import { sanitizeMcpName } from "../../utils/mcp-name"
-
+// kilocode_change start - MCP OAuth Authorization
+import { McpOAuthService, OAuthTokens } from "./oauth"
+// kilocode_change end
 // Discriminated union for connection states
 export type ConnectedMcpConnection = {
 	type: "connected"
@@ -59,6 +63,23 @@ export enum DisableReason {
 	SERVER_DISABLED = "serverDisabled",
 }
 
+// OAuth configuration schema for HTTP-based transports
+// kilocode_change start - MCP OAuth Authorization
+const OAuthConfigSchema = z
+	.object({
+		// Override client_id if pre-registered
+		clientId: z.string().optional(),
+		clientSecret: z.string().optional(),
+
+		// Override scopes to request
+		scopes: z.array(z.string()).optional(),
+
+		// Disable OAuth for this server (use static headers instead)
+		disabled: z.boolean().optional(),
+	})
+	.optional()
+// kilocode_change end
+
 // Base configuration schema for common settings
 const BaseConfigSchema = z.object({
 	disabled: z.boolean().optional(),
@@ -105,6 +126,7 @@ const createServerTypeSchema = () => {
 			type: z.enum(["sse"]).optional(),
 			url: z.string().url("URL must be a valid URL format"),
 			headers: z.record(z.string()).optional(),
+			oauth: OAuthConfigSchema, // kilocode_change - MCP OAuth Authorization
 			// Ensure no stdio fields are present
 			command: z.undefined().optional(),
 			args: z.undefined().optional(),
@@ -120,6 +142,7 @@ const createServerTypeSchema = () => {
 			type: z.enum(["streamable-http"]).optional(),
 			url: z.string().url("URL must be a valid URL format"),
 			headers: z.record(z.string()).optional(),
+			oauth: OAuthConfigSchema, // kilocode_change - MCP OAuth Authorization
 			// Ensure no stdio fields are present
 			command: z.undefined().optional(),
 			args: z.undefined().optional(),
@@ -158,15 +181,434 @@ export class McpHub {
 	private isProgrammaticUpdate: boolean = false
 	private flagResetTimer?: NodeJS.Timeout
 	private sanitizedNameRegistry: Map<string, string> = new Map()
+	// kilocode_change start - MCP OAuth Authorization
+	private oauthService?: McpOAuthService
+	// kilocode_change end
+	// kilocode_change start - Auto-reconnect on disconnect
+	private reconnectAttempts: Map<string, number> = new Map()
+	private reconnectTimers: Map<string, NodeJS.Timeout> = new Map()
+	private static readonly MAX_RECONNECT_ATTEMPTS = 5
+	private static readonly INITIAL_RECONNECT_DELAY_MS = 1000
+	private static readonly MAX_RECONNECT_DELAY_MS = 30000
+	// kilocode_change end
 
 	constructor(provider: ClineProvider) {
 		this.providerRef = new WeakRef(provider)
+		// kilocode_change start - MCP OAuth Authorization
+		this.initializeOAuthService()
+		// kilocode_change end
 		this.watchMcpSettingsFile()
 		this.watchProjectMcpFile().catch(console.error)
 		this.setupWorkspaceFoldersWatcher()
 		this.initializeGlobalMcpServers()
 		this.initializeProjectMcpServers()
 	}
+
+	// kilocode_change start - MCP OAuth Authorization
+	/**
+	 * Initializes the OAuth service if a context is available
+	 */
+	private initializeOAuthService(): void {
+		const provider = this.providerRef.deref()
+		if (provider?.context) {
+			this.oauthService = new McpOAuthService(provider.context)
+		}
+	}
+
+	/**
+	 * Gets OAuth tokens for an HTTP-based server, refreshing if needed
+	 * @param serverUrl The MCP server URL
+	 * @param oauthConfig Optional OAuth configuration overrides
+	 * @returns OAuth tokens if available, null otherwise
+	 */
+	private async getOAuthTokensForServer(
+		serverUrl: string,
+		oauthConfig?: {
+			clientId?: string
+			clientSecret?: string
+			scopes?: string[]
+			disabled?: boolean
+		},
+	): Promise<OAuthTokens | null> {
+		// If OAuth is explicitly disabled for this server, skip
+		if (oauthConfig?.disabled) {
+			return null
+		}
+
+		if (!this.oauthService) {
+			return null
+		}
+
+		try {
+			// Check if tokens need refresh
+			const { needsRefresh, canRefresh, tokens } = await this.oauthService.checkTokenRefreshNeeded(serverUrl)
+
+			if (!tokens) {
+				// No stored tokens - will need OAuth flow when server returns 401
+				return null
+			}
+
+			if (needsRefresh) {
+				if (canRefresh) {
+					console.log(`Token for ${serverUrl} is expired or expiring soon, attempting refresh`)
+					const refreshedTokens = await this.oauthService.refreshAccessToken(serverUrl)
+					if (refreshedTokens) {
+						console.log(`Successfully refreshed token for ${serverUrl}`)
+						return refreshedTokens
+					}
+					// Refresh failed - the tokens might still work if not fully expired yet
+					console.log(`Token refresh failed for ${serverUrl}, using existing tokens`)
+				} else {
+					console.log(
+						`Token for ${serverUrl} needs refresh but cannot refresh (no refresh token or metadata)`,
+					)
+				}
+			}
+
+			return tokens
+		} catch (error) {
+			console.error(`Failed to get OAuth tokens for ${serverUrl}:`, error)
+			return null
+		}
+	}
+
+	/**
+	 * Initiates OAuth flow for a server that requires authentication
+	 * @param serverUrl The MCP server URL
+	 * @param wwwAuthenticateHeader The WWW-Authenticate header from 401 response
+	 * @param oauthConfig Optional OAuth configuration overrides
+	 * @returns OAuth tokens if successful
+	 */
+	async initiateOAuthForServer(
+		serverUrl: string,
+		wwwAuthenticateHeader?: string,
+		oauthConfig?: {
+			clientId?: string
+			clientSecret?: string
+			scopes?: string[]
+		},
+	): Promise<OAuthTokens | null> {
+		if (!this.oauthService) {
+			console.error("OAuth service not initialized")
+			return null
+		}
+
+		try {
+			// Show notification to user
+			vscode.window.showInformationMessage(
+				t("mcp:info.oauth_required", { serverUrl }) || `MCP server requires authentication: ${serverUrl}`,
+			)
+
+			const tokens = await this.oauthService.initiateOAuthFlow(serverUrl, wwwAuthenticateHeader, {
+				clientId: oauthConfig?.clientId,
+				clientSecret: oauthConfig?.clientSecret,
+				scopes: oauthConfig?.scopes,
+			})
+
+			vscode.window.showInformationMessage(
+				t("mcp:info.oauth_success", { serverUrl }) || `Successfully authenticated with: ${serverUrl}`,
+			)
+
+			return tokens
+		} catch (error) {
+			console.error(`OAuth flow failed for ${serverUrl}:`, error)
+			vscode.window.showErrorMessage(
+				t("mcp:errors.oauth_failed", { serverUrl, error: String(error) }) ||
+					`OAuth authentication failed for ${serverUrl}: ${error}`,
+			)
+			return null
+		}
+	}
+
+	/**
+	 * Clears OAuth tokens for a server (for logout/re-auth)
+	 * @param serverUrl The MCP server URL
+	 */
+	async clearOAuthTokens(serverUrl: string): Promise<void> {
+		if (this.oauthService) {
+			await this.oauthService.clearTokens(serverUrl)
+		}
+	}
+
+	/**
+	 * Initiates OAuth sign-in for a server by name (called from webview)
+	 * @param serverName The MCP server name
+	 * @param source The server source (global or project)
+	 * @returns Promise<void>
+	 */
+	async initiateOAuthSignIn(serverName: string, source?: "global" | "project"): Promise<void> {
+		const connection = this.findConnection(serverName, source)
+		if (!connection) {
+			throw new Error(`Server ${serverName} not found`)
+		}
+
+		// Parse the config to get the URL
+		const config = JSON.parse(connection.server.config)
+		if (config.type !== "sse" && config.type !== "streamable-http") {
+			throw new Error(`Server ${serverName} is not an HTTP-based server`)
+		}
+
+		const serverUrl = config.url
+		if (!serverUrl) {
+			throw new Error(`Server ${serverName} does not have a URL configured`)
+		}
+
+		// Get OAuth config overrides if any
+		const oauthConfig = config.oauth
+
+		// Only clear tokens to force re-authentication, but keep client credentials
+		// from Dynamic Client Registration to reuse the registered client_id
+		if (this.oauthService) {
+			console.log(`Clearing stored tokens for re-authentication (keeping client credentials)...`)
+			await this.oauthService.clearTokens(serverUrl)
+		}
+
+		// Initiate the OAuth flow
+		const tokens = await this.initiateOAuthForServer(serverUrl, undefined, {
+			clientId: oauthConfig?.clientId,
+			clientSecret: oauthConfig?.clientSecret,
+			scopes: oauthConfig?.scopes,
+		})
+
+		if (tokens) {
+			// OAuth successful - restart the connection to use the new tokens
+			await this.restartConnection(serverName, connection.server.source)
+		}
+	}
+
+	/**
+	 * Builds the auth status for an HTTP-based server
+	 * @param serverUrl The MCP server URL
+	 * @param oauthTokens The OAuth tokens if available
+	 * @param oauthConfig OAuth configuration for the server
+	 * @param hasStaticAuth Whether static auth headers are configured
+	 * @param debugInfo Optional debug information about the OAuth tokens
+	 * @returns The McpAuthStatus object
+	 */
+	private buildAuthStatus(
+		serverUrl: string,
+		oauthTokens: OAuthTokens | null,
+		oauthConfig?: { disabled?: boolean },
+		hasStaticAuth?: boolean,
+		debugInfo?: McpAuthDebugInfo | null,
+	): McpAuthStatus {
+		// OAuth is explicitly disabled - check for static auth
+		if (oauthConfig?.disabled) {
+			return {
+				method: hasStaticAuth ? "static" : "none",
+				status: hasStaticAuth ? "authenticated" : "none",
+			}
+		}
+
+		// Have OAuth tokens
+		if (oauthTokens) {
+			const isExpired = oauthTokens.expiresAt ? oauthTokens.expiresAt < Date.now() : false
+			const hasRefreshToken = !!oauthTokens.refreshToken
+			return {
+				method: "oauth",
+				// If expired but has refresh token, still consider it authenticated (will auto-refresh)
+				status: isExpired && !hasRefreshToken ? "expired" : "authenticated",
+				expiresAt: oauthTokens.expiresAt,
+				scopes: oauthTokens.scope?.split(" "),
+				debug: debugInfo || undefined,
+			}
+		}
+
+		// No OAuth tokens - check if static auth is configured
+		if (hasStaticAuth) {
+			return {
+				method: "static",
+				status: "authenticated",
+			}
+		}
+
+		// No auth configured - may require OAuth
+		return {
+			method: "none",
+			status: "none",
+		}
+	}
+
+	/**
+	 * Checks if an error indicates OAuth authentication is required (401 response)
+	 * @param error The error to check
+	 * @returns True if the error is a 401 requiring OAuth
+	 */
+	private isOAuthRequiredError(error: unknown): boolean {
+		if (error instanceof Error) {
+			const message = error.message.toLowerCase()
+			// Check for 401 status code in error message or error object properties
+			if (
+				message.includes("401") ||
+				message.includes("unauthorized") ||
+				message.includes("invalid_token") ||
+				message.includes("missing or invalid access token")
+			) {
+				return true
+			}
+			// Check for code property on the error
+			if ((error as any).code === 401) {
+				return true
+			}
+		}
+		return false
+	}
+
+	/**
+	 * Extracts WWW-Authenticate header value from error if available
+	 * @param error The error to check
+	 * @returns The WWW-Authenticate header value or undefined
+	 */
+	private extractWwwAuthenticateHeader(error: unknown): string | undefined {
+		if (error instanceof Error) {
+			// Check if the error has headers
+			const headers = (error as any).headers
+			if (headers) {
+				return headers["www-authenticate"] || headers["WWW-Authenticate"]
+			}
+		}
+		return undefined
+	}
+	/**
+	 * Normalizes OAuth token type to proper HTTP Authorization header casing.
+	 * OAuth servers may return "bearer" (lowercase) but HTTP headers typically use "Bearer" (title case).
+	 * Most authorization validation libraries expect title case.
+	 * @param tokenType The token type from OAuth response (e.g., "bearer", "Bearer", "BEARER")
+	 * @returns The normalized token type with proper casing (e.g., "Bearer")
+	 */
+	private normalizeTokenType(tokenType: string): string {
+		// Handle common token types with proper casing
+		const lowerType = tokenType.toLowerCase()
+		switch (lowerType) {
+			case "bearer":
+				return "Bearer"
+			case "basic":
+				return "Basic"
+			case "digest":
+				return "Digest"
+			case "hoba":
+				return "HOBA"
+			case "mutual":
+				return "Mutual"
+			case "negotiate":
+				return "Negotiate"
+			case "oauth":
+				return "OAuth"
+			case "scram-sha-1":
+				return "SCRAM-SHA-1"
+			case "scram-sha-256":
+				return "SCRAM-SHA-256"
+			case "vapid":
+				return "vapid"
+			default:
+				// For unknown types, capitalize first letter
+				return tokenType.charAt(0).toUpperCase() + tokenType.slice(1).toLowerCase()
+		}
+	}
+	/**
+	 * Schedules an automatic reconnection attempt for a disconnected server.
+	 * Uses exponential backoff with a maximum number of attempts.
+	 * @param serverName The name of the server to reconnect
+	 * @param source The server source (global or project)
+	 */
+	private scheduleReconnect(serverName: string, source: "global" | "project"): void {
+		const key = `${source}-${serverName}`
+
+		// Don't schedule if already scheduled or if hub is disposed
+		if (this.reconnectTimers.has(key) || this.isDisposed) {
+			return
+		}
+
+		const attempts = this.reconnectAttempts.get(key) || 0
+
+		// Check if we've exceeded max attempts
+		if (attempts >= McpHub.MAX_RECONNECT_ATTEMPTS) {
+			console.log(
+				`Max reconnect attempts (${McpHub.MAX_RECONNECT_ATTEMPTS}) reached for "${serverName}", giving up`,
+			)
+			this.reconnectAttempts.delete(key)
+			return
+		}
+
+		// Calculate delay with exponential backoff
+		const delayMs = Math.min(
+			McpHub.INITIAL_RECONNECT_DELAY_MS * Math.pow(2, attempts),
+			McpHub.MAX_RECONNECT_DELAY_MS,
+		)
+
+		console.log(`Scheduling reconnect for "${serverName}" in ${delayMs}ms (attempt ${attempts + 1})`)
+
+		const timer = setTimeout(async () => {
+			this.reconnectTimers.delete(key)
+
+			// Check if server is still disconnected and not disabled
+			const connection = this.findConnection(serverName, source)
+			if (!connection || connection.server.status !== "disconnected" || connection.server.disabled) {
+				// Server is no longer disconnected or was disabled, clear attempts
+				this.reconnectAttempts.delete(key)
+				return
+			}
+
+			// Check if the server requires OAuth authentication
+			if (connection.server.authStatus?.status === "required") {
+				// Don't auto-reconnect if OAuth is required - user must sign in
+				console.log(`Skipping auto-reconnect for "${serverName}" - OAuth authentication required`)
+				this.reconnectAttempts.delete(key)
+				return
+			}
+
+			// Increment attempt counter
+			this.reconnectAttempts.set(key, attempts + 1)
+
+			try {
+				console.log(`Auto-reconnecting to "${serverName}" (attempt ${attempts + 1})`)
+				await this.restartConnection(serverName, source)
+
+				// Check if reconnection was successful
+				const updatedConnection = this.findConnection(serverName, source)
+				if (updatedConnection?.server.status === "connected") {
+					console.log(`Successfully reconnected to "${serverName}"`)
+					this.reconnectAttempts.delete(key)
+				} else {
+					// Still disconnected, schedule another attempt
+					this.scheduleReconnect(serverName, source)
+				}
+			} catch (error) {
+				console.error(`Failed to reconnect to "${serverName}":`, error)
+				// Schedule another attempt
+				this.scheduleReconnect(serverName, source)
+			}
+		}, delayMs)
+
+		this.reconnectTimers.set(key, timer)
+	}
+
+	/**
+	 * Cancels any scheduled reconnect for a server.
+	 * @param serverName The name of the server
+	 * @param source The server source (global or project)
+	 */
+	private cancelReconnect(serverName: string, source: "global" | "project"): void {
+		const key = `${source}-${serverName}`
+		const timer = this.reconnectTimers.get(key)
+		if (timer) {
+			clearTimeout(timer)
+			this.reconnectTimers.delete(key)
+		}
+		this.reconnectAttempts.delete(key)
+	}
+
+	/**
+	 * Resets the reconnect attempt counter for a server.
+	 * Call this when a server successfully connects.
+	 * @param serverName The name of the server
+	 * @param source The server source (global or project)
+	 */
+	private resetReconnectAttempts(serverName: string, source: "global" | "project"): void {
+		const key = `${source}-${serverName}`
+		this.reconnectAttempts.delete(key)
+	}
+	// kilocode_change end
 	/**
 	 * Registers a client (e.g., ClineProvider) using this hub.
 	 * Increments the reference count.
@@ -757,6 +1199,8 @@ export class McpHub {
 						this.appendErrorMessage(connection, error instanceof Error ? error.message : `${error}`)
 					}
 					await this.notifyWebviewOfServerChanges()
+					// kilocode_change - Schedule auto-reconnect on error
+					this.scheduleReconnect(name, source)
 				}
 
 				transport.onclose = async () => {
@@ -765,6 +1209,8 @@ export class McpHub {
 						connection.server.status = "disconnected"
 					}
 					await this.notifyWebviewOfServerChanges()
+					// kilocode_change - Schedule auto-reconnect on close
+					this.scheduleReconnect(name, source)
 				}
 
 				// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
@@ -797,9 +1243,21 @@ export class McpHub {
 				}
 			} else if (configInjected.type === "streamable-http") {
 				// Streamable HTTP connection
+				// kilocode_change start - MCP OAuth Authorization: Inject OAuth tokens if available
+				let httpHeaders: Record<string, string> = { ...(configInjected.headers || {}) }
+				const oauthConfig = (configInjected as any).oauth
+				if (!oauthConfig?.disabled) {
+					const oauthTokens = await this.getOAuthTokensForServer(configInjected.url, oauthConfig)
+					if (oauthTokens) {
+						httpHeaders["Authorization"] =
+							`${this.normalizeTokenType(oauthTokens.tokenType)} ${oauthTokens.accessToken}`
+					}
+				}
+				// kilocode_change end
+
 				transport = new StreamableHTTPClientTransport(new URL(configInjected.url), {
 					requestInit: {
-						headers: configInjected.headers,
+						headers: httpHeaders,
 					},
 				})
 
@@ -812,6 +1270,8 @@ export class McpHub {
 						this.appendErrorMessage(connection, error instanceof Error ? error.message : `${error}`)
 					}
 					await this.notifyWebviewOfServerChanges()
+					// kilocode_change - Schedule auto-reconnect on error
+					this.scheduleReconnect(name, source)
 				}
 
 				transport.onclose = async () => {
@@ -820,20 +1280,37 @@ export class McpHub {
 						connection.server.status = "disconnected"
 					}
 					await this.notifyWebviewOfServerChanges()
+					// kilocode_change - Schedule auto-reconnect on close
+					this.scheduleReconnect(name, source)
 				}
 			} else if (configInjected.type === "sse") {
 				// SSE connection
+				// kilocode_change start - MCP OAuth Authorization: Inject OAuth tokens if available
+				let sseHeaders: Record<string, string> = { ...(configInjected.headers || {}) }
+				const sseOauthConfig = (configInjected as any).oauth
+				if (!sseOauthConfig?.disabled) {
+					const sseOauthTokens = await this.getOAuthTokensForServer(configInjected.url, sseOauthConfig)
+					if (sseOauthTokens) {
+						sseHeaders["Authorization"] =
+							`${this.normalizeTokenType(sseOauthTokens.tokenType)} ${sseOauthTokens.accessToken}`
+					}
+				}
+				// kilocode_change end
+
 				const sseOptions = {
 					requestInit: {
-						headers: configInjected.headers,
+						headers: sseHeaders,
 					},
 				}
 				// Configure ReconnectingEventSource options
 				const reconnectingEventSourceOptions = {
 					max_retry_time: 5000, // Maximum retry time in milliseconds
-					withCredentials: configInjected.headers?.["Authorization"] ? true : false, // Enable credentials if Authorization header exists
+					withCredentials: sseHeaders?.["Authorization"] ? true : false, // Enable credentials if Authorization header exists
 					fetch: (url: string | URL, init: RequestInit) => {
-						const headers = new Headers({ ...(init?.headers || {}), ...(configInjected.headers || {}) })
+						const headers = new Headers(init?.headers)
+						for (const [key, value] of Object.entries(sseHeaders)) {
+							headers.set(key, value)
+						}
 						return fetch(url, {
 							...init,
 							headers,
@@ -855,6 +1332,8 @@ export class McpHub {
 						this.appendErrorMessage(connection, error instanceof Error ? error.message : `${error}`)
 					}
 					await this.notifyWebviewOfServerChanges()
+					// kilocode_change - Schedule auto-reconnect on error
+					this.scheduleReconnect(name, source)
 				}
 
 				transport.onclose = async () => {
@@ -863,6 +1342,8 @@ export class McpHub {
 						connection.server.status = "disconnected"
 					}
 					await this.notifyWebviewOfServerChanges()
+					// kilocode_change - Schedule auto-reconnect on close
+					this.scheduleReconnect(name, source)
 				}
 			} else {
 				// Should not happen if validateServerConfig is correct
@@ -874,6 +1355,40 @@ export class McpHub {
 				transport.start = async () => {}
 			}
 
+			// kilocode_change start - MCP OAuth Authorization: Build auth status for HTTP-based transports
+			let authStatus: McpAuthStatus | undefined
+			if (configInjected.type === "streamable-http" || configInjected.type === "sse") {
+				const httpOauthConfig = (configInjected as any).oauth
+				const hasStaticAuth = !!configInjected.headers?.["Authorization"]
+				// Get stored tokens for auth status (we already fetched them above during token injection)
+				const storedTokens = !httpOauthConfig?.disabled
+					? await this.getOAuthTokensForServer(configInjected.url, httpOauthConfig)
+					: null
+				// Get debug info for the auth status
+				let debugInfo: McpAuthDebugInfo | null = null
+				if (this.oauthService && storedTokens) {
+					const tokenDebugInfo = await this.oauthService.getTokenDebugInfo(configInjected.url)
+					if (tokenDebugInfo) {
+						debugInfo = {
+							issuedAt: tokenDebugInfo.issuedAt,
+							hasRefreshToken: tokenDebugInfo.hasRefreshToken,
+							tokenEndpoint: tokenDebugInfo.tokenEndpoint,
+							clientId: tokenDebugInfo.clientId,
+							canRefresh: tokenDebugInfo.canRefresh,
+							nextRefreshAt: tokenDebugInfo.nextRefreshAt,
+						}
+					}
+				}
+				authStatus = this.buildAuthStatus(
+					configInjected.url,
+					storedTokens,
+					httpOauthConfig,
+					hasStaticAuth,
+					debugInfo,
+				)
+			}
+			// kilocode_change end
+
 			// Create a connected connection
 			const connection: ConnectedMcpConnection = {
 				type: "connected",
@@ -885,6 +1400,9 @@ export class McpHub {
 					source,
 					projectPath: source === "project" ? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath : undefined,
 					errorHistory: [],
+					// kilocode_change start - MCP OAuth Authorization
+					authStatus,
+					// kilocode_change end
 				},
 				client,
 				transport,
@@ -896,12 +1414,44 @@ export class McpHub {
 			connection.server.status = "connected"
 			connection.server.error = ""
 			connection.server.instructions = client.getInstructions()
+			// kilocode_change - Reset reconnect attempts on successful connection
+			this.resetReconnectAttempts(name, source)
 
 			this.kiloNotificationService.connect(name, connection.client)
 
 			// Initial fetch of tools and resources
 			await this.fetchAvailableServerCapabilities(name, source) // kilocode_change: logic moved into method
 		} catch (error) {
+			// kilocode_change start - MCP OAuth Authorization: Handle 401 errors
+			// Check if this is an HTTP-based transport and if the error indicates OAuth is required
+			// Instead of automatically opening the auth flow, we just set the status to "required"
+			// and let the user click "Sign in" to trigger the OAuth flow
+			if (
+				(config.type === "streamable-http" || config.type === "sse") &&
+				this.isOAuthRequiredError(error) &&
+				!(config as any).oauth?.disabled
+			) {
+				console.log(`OAuth required for "${name}", showing sign-in button (not auto-opening auth flow)`)
+
+				// Remove the failed connection before creating placeholder
+				await this.deleteConnection(name, source)
+
+				// Create disconnected connection with auth status showing sign-in is required
+				// The user must click "Sign in" to initiate the OAuth flow
+				const connection = this.createPlaceholderConnection(name, config, source, DisableReason.SERVER_DISABLED)
+				connection.server.disabled = false // Not actually disabled, just requires auth
+				connection.server.status = "disconnected"
+				connection.server.error = "Authentication required. Click 'Sign in' to authenticate."
+				connection.server.authStatus = {
+					method: "oauth",
+					status: "required",
+				}
+				this.connections.push(connection)
+				await this.notifyWebviewOfServerChanges()
+				return
+			}
+			// kilocode_change end
+
 			// Update status with error
 			const connection = this.findConnection(name, source)
 			if (connection) {
@@ -1121,6 +1671,15 @@ export class McpHub {
 		// Clean up file watchers for this server
 		this.removeFileWatchersForServer(name)
 
+		// kilocode_change - Cancel any pending reconnect attempts
+		if (source) {
+			this.cancelReconnect(name, source)
+		} else {
+			// Cancel for both sources if not specified
+			this.cancelReconnect(name, "global")
+			this.cancelReconnect(name, "project")
+		}
+
 		// If source is provided, only delete connections from that source
 		const connections = source
 			? this.connections.filter((conn) => conn.server.name === name && conn.server.source === source)
@@ -2020,6 +2579,13 @@ export class McpHub {
 		}
 		this.isProgrammaticUpdate = false
 
+		// kilocode_change - Clear all reconnect timers
+		for (const timer of this.reconnectTimers.values()) {
+			clearTimeout(timer)
+		}
+		this.reconnectTimers.clear()
+		this.reconnectAttempts.clear()
+
 		this.removeAllFileWatchers()
 		for (const connection of this.connections) {
 			try {

+ 181 - 0
src/services/mcp/oauth/McpAuthorizationDiscovery.ts

@@ -0,0 +1,181 @@
+import { z } from "zod"
+import { parseWwwAuthenticateHeader } from "./utils"
+
+// Zod schemas for runtime validation at IO boundaries
+
+/**
+ * Protected Resource Metadata schema (RFC 9728)
+ */
+export const ProtectedResourceMetadataSchema = z
+	.object({
+		resource: z.string(),
+		authorization_servers: z.array(z.string()),
+		scopes_supported: z.array(z.string()).optional(),
+	})
+	.passthrough() // Allow additional RFC-defined fields
+
+export type ProtectedResourceMetadata = z.infer<typeof ProtectedResourceMetadataSchema>
+
+/**
+ * Authorization Server Metadata schema (RFC 8414 / OIDC Discovery)
+ * Note: We make authorization_endpoint and token_endpoint optional here and validate
+ * them separately to provide better error messages and support various OAuth flows.
+ */
+export const AuthorizationServerMetadataSchema = z
+	.object({
+		issuer: z.string(),
+		authorization_endpoint: z.string().optional(),
+		token_endpoint: z.string().optional(),
+		scopes_supported: z.array(z.string()).optional(),
+		response_types_supported: z.array(z.string()).optional(),
+		code_challenge_methods_supported: z.array(z.string()).optional(),
+		client_id_metadata_document_supported: z.boolean().optional(),
+		registration_endpoint: z.string().optional(),
+		// Device flow support
+		device_authorization_endpoint: z.string().optional(),
+	})
+	.passthrough() // Allow additional RFC-defined fields
+
+export type AuthorizationServerMetadata = z.infer<typeof AuthorizationServerMetadataSchema>
+
+export class McpAuthorizationDiscovery {
+	/**
+	 * Discovers authorization server from WWW-Authenticate header or well-known URIs
+	 */
+	async discoverAuthorizationServer(
+		serverUrl: string,
+		wwwAuthenticateHeader?: string,
+	): Promise<AuthorizationServerMetadata> {
+		// 1. Get resource metadata URL from WWW-Authenticate header
+		let metadataUrl: string | null = null
+		if (wwwAuthenticateHeader) {
+			metadataUrl = parseWwwAuthenticateHeader(wwwAuthenticateHeader)
+		}
+
+		// 2. Try to fetch resource metadata from various sources
+		let resourceMetadata: ProtectedResourceMetadata | null = null
+		let lastError: Error | null = null
+
+		// Try 1: Use URL from WWW-Authenticate header
+		if (metadataUrl) {
+			try {
+				resourceMetadata = await this.fetchResourceMetadata(metadataUrl)
+			} catch (e) {
+				console.log(`Failed to fetch resource metadata from WWW-Authenticate URL: ${e}`)
+				lastError = e instanceof Error ? e : new Error(String(e))
+			}
+		}
+
+		// Try 2: Well-known URI according to RFC 9728
+		if (!resourceMetadata) {
+			const wellKnownUrl = this.buildWellKnownResourceMetadataUrl(serverUrl)
+			try {
+				resourceMetadata = await this.fetchResourceMetadata(wellKnownUrl)
+			} catch (e) {
+				console.log(`Failed to fetch resource metadata from well-known URL: ${e}`)
+				lastError = e instanceof Error ? e : new Error(String(e))
+			}
+		}
+
+		// Try 3: Directly discover auth server metadata at server origin
+		// This is a fallback for servers that don't implement RFC 9728 but do have OAuth
+		if (!resourceMetadata) {
+			console.log("No resource metadata available, trying direct auth server discovery at server origin")
+			try {
+				const url = new URL(serverUrl)
+				return await this.fetchAuthServerMetadata(url.origin)
+			} catch (e) {
+				console.log(`Failed direct auth server discovery: ${e}`)
+				// Continue to throw the original error
+			}
+		}
+
+		if (!resourceMetadata) {
+			throw lastError || new Error("Failed to discover authorization server")
+		}
+
+		// 3. Pick first auth server
+		if (!resourceMetadata.authorization_servers || resourceMetadata.authorization_servers.length === 0) {
+			throw new Error("No authorization servers found in resource metadata")
+		}
+		const authServerUrl = resourceMetadata.authorization_servers[0]
+
+		// 4. Fetch auth server metadata
+		return this.fetchAuthServerMetadata(authServerUrl)
+	}
+
+	/**
+	 * Constructs the well-known URL for Protected Resource Metadata according to RFC 9728.
+	 * For a resource at https://example.com/path, the metadata URL is:
+	 * https://example.com/.well-known/oauth-protected-resource/path
+	 */
+	private buildWellKnownResourceMetadataUrl(serverUrl: string): string {
+		const url = new URL(serverUrl)
+		const path = url.pathname.replace(/\/$/, "") // Remove trailing slash if present
+		// Insert .well-known path after origin, before any resource path
+		url.pathname = `/.well-known/oauth-protected-resource${path}`
+		return url.toString()
+	}
+
+	/**
+	 * Fetches Protected Resource Metadata (RFC 9728)
+	 */
+	async fetchResourceMetadata(metadataUrl: string): Promise<ProtectedResourceMetadata> {
+		try {
+			const response = await fetch(metadataUrl)
+			if (!response.ok) {
+				throw new Error(`HTTP ${response.status} ${response.statusText}`)
+			}
+			const json: unknown = await response.json()
+			const result = ProtectedResourceMetadataSchema.safeParse(json)
+			if (!result.success) {
+				throw new Error(`Invalid resource metadata format: ${result.error.message}`)
+			}
+			return result.data
+		} catch (error) {
+			throw new Error(`Failed to fetch resource metadata from ${metadataUrl}: ${error}`)
+		}
+	}
+
+	/**
+	 * Fetches Authorization Server Metadata (RFC 8414 / OIDC Discovery)
+	 */
+	async fetchAuthServerMetadata(issuerUrl: string): Promise<AuthorizationServerMetadata> {
+		const baseUrl = issuerUrl.replace(/\/$/, "")
+
+		// Try RFC 8414 first
+		try {
+			const url = `${baseUrl}/.well-known/oauth-authorization-server`
+			const response = await fetch(url)
+			if (response.ok) {
+				const json: unknown = await response.json()
+				const result = AuthorizationServerMetadataSchema.safeParse(json)
+				if (result.success && result.data.authorization_endpoint && result.data.token_endpoint) {
+					return result.data
+				}
+				// Log validation error but continue to try OIDC
+				console.warn(`RFC 8414 metadata incomplete or validation failed`)
+			}
+		} catch (e) {
+			// Ignore and try next
+		}
+
+		// Try OIDC Discovery
+		try {
+			const url = `${baseUrl}/.well-known/openid-configuration`
+			const response = await fetch(url)
+			if (response.ok) {
+				const json: unknown = await response.json()
+				const result = AuthorizationServerMetadataSchema.safeParse(json)
+				if (result.success && result.data.authorization_endpoint && result.data.token_endpoint) {
+					return result.data
+				}
+				console.warn(`OIDC metadata incomplete or validation failed`)
+			}
+		} catch (e) {
+			// Ignore fetch errors
+		}
+
+		throw new Error(`Failed to discover authorization server metadata for ${issuerUrl}`)
+	}
+}

+ 235 - 0
src/services/mcp/oauth/McpOAuthBrowserFlow.ts

@@ -0,0 +1,235 @@
+import * as http from "http"
+import * as vscode from "vscode"
+import { URL } from "url"
+
+/**
+ * Default port for the authorization flow. We try to use this port so that
+ * the redirect URI does not change when running on localhost. This is useful
+ * for servers that only allow exact matches on the redirect URI. The spec
+ * says that the port should not matter, but some servers do not follow
+ * the spec and require an exact match.
+ *
+ * This matches VSCode's default port for consistency.
+ */
+export const DEFAULT_AUTH_FLOW_PORT = 33418
+
+export interface AuthorizationParams {
+	authorizationEndpoint: string
+	clientId: string
+	redirectUri: string // This might be overridden if we use local server
+	scope?: string // Optional - some servers don't support scope
+	state: string
+	codeChallenge: string
+	codeChallengeMethod: "S256"
+	resource?: string // Optional - some servers don't support RFC 8707 resource parameter
+}
+
+export interface AuthorizationResult {
+	code: string
+	state: string
+	redirectUri: string
+}
+
+export class McpOAuthBrowserFlow {
+	/**
+	 * Opens browser for authorization and waits for callback
+	 */
+	async authorize(params: AuthorizationParams): Promise<AuthorizationResult> {
+		// Try to start local server
+		try {
+			return await this.authorizeWithLocalServer(params)
+		} catch (error) {
+			console.warn("Failed to use local server for OAuth, falling back to URI handler", error)
+			// Fallback to URI handler would go here
+			throw error
+		}
+	}
+
+	private async authorizeWithLocalServer(params: AuthorizationParams): Promise<AuthorizationResult> {
+		return new Promise((resolve, reject) => {
+			const server = http.createServer(async (req, res) => {
+				try {
+					const url = new URL(req.url || "", `http://127.0.0.1:${(server.address() as any).port}`)
+
+					// Accept both root path "/" and "/callback" for compatibility
+					if (url.pathname !== "/" && url.pathname !== "/callback") {
+						res.writeHead(404)
+						res.end("Not Found")
+						return
+					}
+
+					const code = url.searchParams.get("code")
+					const state = url.searchParams.get("state")
+					const error = url.searchParams.get("error")
+
+					if (error) {
+						res.writeHead(400)
+						res.end(`Authentication failed: ${error}`)
+						reject(new Error(`OAuth error: ${error}`))
+						return
+					}
+
+					if (!code || !state) {
+						res.writeHead(400)
+						res.end("Missing code or state")
+						reject(new Error("Missing code or state"))
+						return
+					}
+
+					// Success response
+					res.writeHead(200, { "Content-Type": "text/html" })
+					res.end(/* html */ `
+	<!DOCTYPE html>
+	<html>
+	<head>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width,initial-scale=1">
+		<title>Kilo Code - Auth Success</title>
+		<style>
+			* { margin: 0; padding: 0; box-sizing: border-box; }
+			body {
+				font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+				background: oklch(21.7% 0.004 107);
+				color: oklch(1 0 0 / 90%);
+				min-height: 100vh;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+			}
+			.card {
+				background: oklch(28.5% 0 0);
+				border-radius: 0.625rem;
+				padding: 2rem;
+				text-align: center;
+				max-width: 320px;
+				box-shadow: 0 4px 24px rgba(0,0,0,0.3);
+			}
+			.icon {
+				width: 48px;
+				height: 48px;
+				background: oklch(63% 0.19 147);
+				border-radius: 50%;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				margin: 0 auto 1rem;
+			}
+			.icon svg {
+				width: 24px;
+				height: 24px;
+				stroke: white;
+				stroke-width: 3;
+			}
+			h1 {
+				font-size: 1.25rem;
+				font-weight: 600;
+				margin-bottom: 0.5rem;
+				color: oklch(95% 0.15 108);
+			}
+			p {
+				font-size: 0.875rem;
+				color: oklch(1 0 0 / 60%);
+			}
+			.brand {
+				margin-top: 1.5rem;
+				font-size: 0.75rem;
+				color: oklch(1 0 0 / 40%);
+			}
+		</style>
+	</head>
+	<body>
+		<div class="card">
+			<div class="icon">
+				<svg viewBox="0 0 24 24" fill="none">
+					<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round"/>
+				</svg>
+			</div>
+			<h1>Authentication Successful</h1>
+			<p>You can close this window and return to VS Code.</p>
+			<div class="brand">Kilo Code</div>
+		</div>
+		<script>setTimeout(() => window.close(), 1500)</script>
+	</body>
+	</html>
+	`)
+
+					// We need to resolve with the redirectUri that was used.
+					// Since we are inside the callback, we know the port.
+					// Use root path with trailing slash to match what we sent in the auth request
+					const port = (server.address() as any).port
+					const redirectUri = `http://127.0.0.1:${port}/`
+
+					resolve({ code, state, redirectUri })
+				} catch (e) {
+					reject(e)
+				} finally {
+					server.close()
+				}
+			})
+
+			const openAuthorizationUrl = async () => {
+				const port = (server.address() as any).port
+				// Use root path with trailing slash for better compatibility with OAuth servers
+				// Some servers (like Cloudflare) expect just the root path, not /callback
+				const redirectUri = `http://127.0.0.1:${port}/`
+
+				// Construct auth URL manually to ensure proper encoding
+				// Note: URL.searchParams.set() uses application/x-www-form-urlencoded encoding
+				// which encodes spaces as '+' but doesn't encode some characters like ':' and '/'
+				// We need to use encodeURIComponent() for proper RFC 3986 percent-encoding
+				const queryParams = new URLSearchParams()
+				queryParams.set("client_id", params.clientId)
+				queryParams.set("response_type", "code")
+				queryParams.set("code_challenge", params.codeChallenge)
+				queryParams.set("code_challenge_method", params.codeChallengeMethod)
+				// Manually encode redirect_uri using encodeURIComponent for proper RFC 3986 encoding
+				queryParams.set("redirect_uri", redirectUri)
+				queryParams.set("state", params.state)
+				// Only include scope if it's defined - some servers don't support it
+				if (params.scope) {
+					queryParams.set("scope", params.scope)
+				}
+				// Only include resource if it's defined - some servers don't support RFC 8707
+				if (params.resource) {
+					queryParams.set("resource", params.resource)
+				}
+
+				// Build URL manually to ensure redirect_uri is properly encoded
+				// The URLSearchParams toString() encodes values, but we need full RFC 3986 encoding
+				const queryString = queryParams.toString()
+
+				const authUrl = `${params.authorizationEndpoint}?${queryString}`
+
+				// Log the full authorization URL for debugging
+				console.log(`Opening authorization URL: ${authUrl}`)
+
+				// Open browser
+				const success = await vscode.env.openExternal(vscode.Uri.parse(authUrl))
+				if (!success) {
+					server.close()
+					reject(new Error("Failed to open browser"))
+				}
+			}
+
+			server.on("listening", () => {
+				openAuthorizationUrl()
+			})
+
+			server.on("error", (err: NodeJS.ErrnoException) => {
+				if (err.code === "EADDRINUSE") {
+					// Default port is in use, try a random port instead
+					console.log(`Port ${DEFAULT_AUTH_FLOW_PORT} is in use, trying a random port...`)
+					server.listen(0, "127.0.0.1")
+				} else {
+					reject(err)
+				}
+			})
+
+			// Try to use the default port first for consistent redirect_uri
+			// This is important because some OAuth servers require exact redirect_uri matches
+			// and we registered specific ports during Dynamic Client Registration
+			console.log(`Attempting to listen on default OAuth port ${DEFAULT_AUTH_FLOW_PORT}...`)
+			server.listen(DEFAULT_AUTH_FLOW_PORT, "127.0.0.1")
+		})
+	}
+}

+ 693 - 0
src/services/mcp/oauth/McpOAuthService.ts

@@ -0,0 +1,693 @@
+import * as vscode from "vscode"
+import { z } from "zod"
+import { McpAuthorizationDiscovery, AuthorizationServerMetadata } from "./McpAuthorizationDiscovery"
+import { McpOAuthBrowserFlow, DEFAULT_AUTH_FLOW_PORT } from "./McpOAuthBrowserFlow"
+import { McpOAuthTokenStorage, OAuthTokens, StoredTokenData } from "./McpOAuthTokenStorage"
+import { generateCodeChallenge, generateCodeVerifier, generateState } from "./utils"
+
+// Buffer time before token expiration to trigger proactive refresh (5 minutes)
+const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000
+
+/**
+ * OAuth configuration options that can override defaults
+ */
+export interface OAuthConfigOptions {
+	/** Override client_id if pre-registered with the server */
+	clientId?: string
+	/** Client secret for confidential clients */
+	clientSecret?: string
+	/** Override scopes to request */
+	scopes?: string[]
+}
+
+/**
+ * Client credentials from Dynamic Client Registration (RFC 7591)
+ * Schema for stored client credentials (camelCase format)
+ */
+const RegisteredClientCredentialsSchema = z.object({
+	clientId: z.string(),
+	clientSecret: z.string().optional(),
+	clientIdIssuedAt: z.number().optional(),
+	clientSecretExpiresAt: z.number().optional(),
+	redirectUris: z.array(z.string()).optional(),
+})
+
+type RegisteredClientCredentials = z.infer<typeof RegisteredClientCredentialsSchema>
+
+/**
+ * Dynamic Client Registration Response schema (RFC 7591)
+ */
+const ClientRegistrationResponseSchema = z
+	.object({
+		client_id: z.string(),
+		client_secret: z.string().optional(),
+		client_id_issued_at: z.number().optional(),
+		client_secret_expires_at: z.number().optional(),
+		// Echo back the registered metadata
+		redirect_uris: z.array(z.string()).optional(),
+		grant_types: z.array(z.string()).optional(),
+		response_types: z.array(z.string()).optional(),
+		token_endpoint_auth_method: z.string().optional(),
+		client_name: z.string().optional(),
+		client_uri: z.string().optional(),
+		logo_uri: z.string().optional(),
+	})
+	.passthrough()
+
+export class McpOAuthService {
+	private discovery: McpAuthorizationDiscovery
+	private browserFlow: McpOAuthBrowserFlow
+	private tokenStorage: McpOAuthTokenStorage
+	private context: vscode.ExtensionContext
+
+	// Storage key prefix for registered client credentials
+	private static readonly CLIENT_CREDENTIALS_PREFIX = "mcp-oauth-client-"
+
+	constructor(context: vscode.ExtensionContext) {
+		this.context = context
+		this.discovery = new McpAuthorizationDiscovery()
+		this.browserFlow = new McpOAuthBrowserFlow()
+		this.tokenStorage = new McpOAuthTokenStorage(context)
+	}
+
+	/**
+	 * Gets stored client credentials for an authorization server
+	 */
+	private async getStoredClientCredentials(authServerUrl: string): Promise<RegisteredClientCredentials | null> {
+		const key = `${McpOAuthService.CLIENT_CREDENTIALS_PREFIX}${this.hashUrl(authServerUrl)}`
+		const stored = await this.context.secrets.get(key)
+		if (!stored) {
+			return null
+		}
+		try {
+			const parsed: unknown = JSON.parse(stored)
+			const result = RegisteredClientCredentialsSchema.safeParse(parsed)
+			if (!result.success) {
+				console.warn(
+					`[McpOAuthService] Invalid stored client credentials for ${authServerUrl}:`,
+					result.error.message,
+				)
+				return null
+			}
+			return result.data
+		} catch {
+			return null
+		}
+	}
+
+	/**
+	 * Stores client credentials for an authorization server
+	 */
+	private async storeClientCredentials(
+		authServerUrl: string,
+		credentials: RegisteredClientCredentials,
+	): Promise<void> {
+		const key = `${McpOAuthService.CLIENT_CREDENTIALS_PREFIX}${this.hashUrl(authServerUrl)}`
+		await this.context.secrets.store(key, JSON.stringify(credentials))
+	}
+
+	/**
+	 * Simple hash function for URL-based storage keys
+	 */
+	private hashUrl(url: string): string {
+		let hash = 0
+		for (let i = 0; i < url.length; i++) {
+			const char = url.charCodeAt(i)
+			hash = (hash << 5) - hash + char
+			hash = hash & hash // Convert to 32-bit integer
+		}
+		return Math.abs(hash).toString(16)
+	}
+
+	/**
+	 * Initiates OAuth flow for an MCP server that returned 401
+	 * @param serverUrl The MCP server URL
+	 * @param wwwAuthenticateHeader The WWW-Authenticate header from 401 response (optional)
+	 * @param options Optional OAuth configuration overrides
+	 * @returns Promise resolving to access token
+	 */
+	async initiateOAuthFlow(
+		serverUrl: string,
+		wwwAuthenticateHeader?: string,
+		options?: OAuthConfigOptions,
+	): Promise<OAuthTokens> {
+		// If WWW-Authenticate header wasn't provided, probe the server to get it
+		let authHeader = wwwAuthenticateHeader
+		if (!authHeader) {
+			authHeader = await this.probeServerForAuthHeader(serverUrl)
+		}
+
+		// 1. Discovery
+		const authServerMetadata = await this.discovery.discoverAuthorizationServer(serverUrl, authHeader)
+
+		// Validate required endpoints exist for authorization code flow
+		if (!authServerMetadata.authorization_endpoint) {
+			throw new Error(
+				`OAuth server at ${authServerMetadata.issuer} does not support authorization code flow (no authorization_endpoint). ` +
+					`It may require a different authentication method.`,
+			)
+		}
+		if (!authServerMetadata.token_endpoint) {
+			throw new Error(
+				`OAuth server at ${authServerMetadata.issuer} does not have a token endpoint (no token_endpoint). ` +
+					`It may require a different authentication method.`,
+			)
+		}
+
+		// 2. Generate PKCE
+		const codeVerifier = generateCodeVerifier()
+		const codeChallenge = generateCodeChallenge(codeVerifier)
+		const state = generateState()
+
+		// 3. Get or register client credentials
+		// The redirect URI will be determined by the browser flow, but we need to specify the pattern
+		// for Dynamic Client Registration. Register multiple URIs for compatibility:
+		// - Generic localhost URI without port (for servers that don't care about port)
+		// - Specific port URI (for servers that require exact match - like Cloudflare)
+		// - VS Code URI handler (for fallback)
+		// This matches VSCode's approach: they register both http://127.0.0.1/ and http://127.0.0.1:33418/
+		const redirectUris = [
+			"http://127.0.0.1/",
+			`http://127.0.0.1:${DEFAULT_AUTH_FLOW_PORT}/`,
+			"vscode://kilocode.kilo-code/oauth/callback",
+		]
+		const clientCredentials = await this.getOrRegisterClient(authServerMetadata, redirectUris, options)
+
+		// Scopes: Use provided scopes, or from metadata if available
+		// Don't use a default - some servers don't support scope parameter at all
+		let scope: string | undefined
+		if (options?.scopes?.length) {
+			scope = options.scopes.join(" ")
+		} else if (authServerMetadata.scopes_supported?.length) {
+			scope = authServerMetadata.scopes_supported.join(" ")
+		}
+		// If no scopes defined anywhere, we'll omit the scope parameter
+
+		// 4. Browser Flow
+		// Note: We don't include the 'resource' parameter by default as some servers
+		// (like Cloudflare) don't support RFC 8707 and return internal server error
+		const authResult = await this.browserFlow.authorize({
+			authorizationEndpoint: authServerMetadata.authorization_endpoint,
+			clientId: clientCredentials.clientId,
+			redirectUri: "http://127.0.0.1:0/callback", // Placeholder, will be replaced by local server
+			scope,
+			state,
+			codeChallenge,
+			codeChallengeMethod: "S256",
+			// resource: serverUrl, // Disabled: Cloudflare doesn't support RFC 8707 resource parameter
+		})
+
+		// 5. Verify State
+		if (authResult.state !== state) {
+			throw new Error("State mismatch")
+		}
+
+		// 6. Exchange Code for Token
+		const tokens = await this.exchangeCodeForToken(
+			authServerMetadata.token_endpoint,
+			authResult.code,
+			codeVerifier,
+			clientCredentials.clientId,
+			authResult.redirectUri,
+			clientCredentials.clientSecret,
+		)
+
+		// 7. Store Tokens with metadata needed for refresh
+		await this.tokenStorage.storeTokens(serverUrl, tokens, {
+			tokenEndpoint: authServerMetadata.token_endpoint,
+			clientId: clientCredentials.clientId,
+			clientSecret: clientCredentials.clientSecret,
+		})
+
+		return tokens
+	}
+
+	/**
+	 * Gets stored tokens for a server, if available and valid
+	 */
+	async getStoredTokens(serverUrl: string): Promise<OAuthTokens | null> {
+		return this.tokenStorage.getTokens(serverUrl)
+	}
+
+	/**
+	 * Clears stored tokens for a server (for logout/re-auth)
+	 */
+	async clearTokens(serverUrl: string): Promise<void> {
+		await this.tokenStorage.removeTokens(serverUrl)
+	}
+
+	/**
+	 * Clears stored client credentials for an authorization server.
+	 * This forces re-registration on the next OAuth flow.
+	 */
+	async clearClientCredentials(authServerUrl: string): Promise<void> {
+		const key = `${McpOAuthService.CLIENT_CREDENTIALS_PREFIX}${this.hashUrl(authServerUrl)}`
+		console.log(`[McpOAuthService] Clearing stored client credentials for: ${authServerUrl} (key: ${key})`)
+		await this.context.secrets.delete(key)
+	}
+
+	/**
+	 * Clears all stored client credentials.
+	 * This forces re-registration for all authorization servers on next OAuth flow.
+	 */
+	async clearAllClientCredentials(): Promise<void> {
+		// Unfortunately, VS Code SecretStorage doesn't have a way to list all keys
+		// So we need to clear known ones. For now, let's clear credentials for common servers.
+		const commonServers = [
+			"https://bindings.mcp.cloudflare.com", // Cloudflare
+			"https://github.com", // GitHub
+		]
+		for (const server of commonServers) {
+			await this.clearClientCredentials(server)
+		}
+		console.log("[McpOAuthService] Cleared all known client credentials")
+	}
+
+	/**
+	 * Checks if stored tokens are expired or about to expire and need refresh
+	 * @param serverUrl The MCP server URL
+	 * @returns Object indicating if refresh is needed and if refresh is possible
+	 */
+	async checkTokenRefreshNeeded(serverUrl: string): Promise<{
+		needsRefresh: boolean
+		canRefresh: boolean
+		tokens: OAuthTokens | null
+	}> {
+		const tokenData = await this.tokenStorage.getFullTokenData(serverUrl)
+
+		if (!tokenData) {
+			return { needsRefresh: false, canRefresh: false, tokens: null }
+		}
+
+		const tokens: OAuthTokens = {
+			accessToken: tokenData.accessToken,
+			tokenType: tokenData.tokenType,
+			expiresAt: tokenData.expiresAt,
+			refreshToken: tokenData.refreshToken,
+			scope: tokenData.scope,
+		}
+
+		// Check if token is expired or about to expire
+		const isExpiredOrExpiring = tokenData.expiresAt
+			? tokenData.expiresAt < Date.now() + TOKEN_REFRESH_BUFFER_MS
+			: false
+
+		if (!isExpiredOrExpiring) {
+			return { needsRefresh: false, canRefresh: false, tokens }
+		}
+
+		// Token needs refresh - check if we can refresh it
+		const canRefresh = !!(tokenData.refreshToken && tokenData.tokenEndpoint && tokenData.clientId)
+
+		return { needsRefresh: true, canRefresh, tokens }
+	}
+
+	/**
+	 * Gets debug information about stored token data for a server
+	 * @param serverUrl The MCP server URL
+	 * @returns Debug info object or null if no tokens stored
+	 */
+	async getTokenDebugInfo(serverUrl: string): Promise<{
+		issuedAt?: number
+		hasRefreshToken: boolean
+		tokenEndpoint?: string
+		clientId?: string
+		canRefresh: boolean
+		nextRefreshAt?: number
+	} | null> {
+		const tokenData = await this.tokenStorage.getFullTokenData(serverUrl)
+
+		if (!tokenData) {
+			return null
+		}
+
+		const hasRefreshToken = !!tokenData.refreshToken
+		const canRefresh = !!(tokenData.refreshToken && tokenData.tokenEndpoint && tokenData.clientId)
+
+		// Calculate next refresh time: TOKEN_REFRESH_BUFFER_MS before expiration
+		let nextRefreshAt: number | undefined
+		if (tokenData.expiresAt && canRefresh) {
+			nextRefreshAt = tokenData.expiresAt - TOKEN_REFRESH_BUFFER_MS
+		}
+
+		return {
+			issuedAt: tokenData.issuedAt,
+			hasRefreshToken,
+			tokenEndpoint: tokenData.tokenEndpoint,
+			clientId: tokenData.clientId,
+			canRefresh,
+			nextRefreshAt,
+		}
+	}
+
+	/**
+	 * Refreshes an access token using the stored refresh token
+	 * @param serverUrl The MCP server URL
+	 * @returns New OAuth tokens if refresh was successful, null if refresh failed
+	 */
+	async refreshAccessToken(serverUrl: string): Promise<OAuthTokens | null> {
+		const tokenData = await this.tokenStorage.getFullTokenData(serverUrl)
+
+		if (!tokenData) {
+			console.log(`[McpOAuthService] No stored token data for ${serverUrl}, cannot refresh`)
+			return null
+		}
+
+		if (!tokenData.refreshToken) {
+			console.log(`[McpOAuthService] No refresh token stored for ${serverUrl}, cannot refresh`)
+			return null
+		}
+
+		if (!tokenData.tokenEndpoint) {
+			console.log(`[McpOAuthService] No token endpoint stored for ${serverUrl}, cannot refresh`)
+			return null
+		}
+
+		if (!tokenData.clientId) {
+			console.log(`[McpOAuthService] No client ID stored for ${serverUrl}, cannot refresh`)
+			return null
+		}
+
+		console.log(`[McpOAuthService] Refreshing access token for ${serverUrl}`)
+
+		try {
+			const body = new URLSearchParams({
+				grant_type: "refresh_token",
+				refresh_token: tokenData.refreshToken,
+				client_id: tokenData.clientId,
+			})
+
+			// Include client_secret if available (for confidential clients)
+			if (tokenData.clientSecret) {
+				body.set("client_secret", tokenData.clientSecret)
+			}
+
+			const response = await fetch(tokenData.tokenEndpoint, {
+				method: "POST",
+				headers: {
+					"Content-Type": "application/x-www-form-urlencoded",
+				},
+				body: body.toString(),
+			})
+
+			if (!response.ok) {
+				const text = await response.text()
+				console.error(
+					`[McpOAuthService] Token refresh failed: ${response.status} ${response.statusText} - ${text}`,
+				)
+
+				// Check if the error indicates the refresh token is invalid/expired
+				// In this case, we should clear tokens and require re-authentication
+				if (response.status === 400 || response.status === 401) {
+					console.log(
+						`[McpOAuthService] Refresh token appears to be invalid/expired, clearing tokens for ${serverUrl}`,
+					)
+					await this.tokenStorage.removeTokens(serverUrl)
+				}
+
+				return null
+			}
+
+			const data = (await response.json()) as any
+
+			// Validate response
+			if (!data.access_token || !data.token_type) {
+				console.error("[McpOAuthService] Invalid token refresh response - missing access_token or token_type")
+				return null
+			}
+
+			const newTokens: OAuthTokens = {
+				accessToken: data.access_token,
+				tokenType: data.token_type,
+				expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined,
+				// Use new refresh token if provided, otherwise keep the old one
+				refreshToken: data.refresh_token || tokenData.refreshToken,
+				scope: data.scope || tokenData.scope,
+			}
+
+			// Store the updated tokens with the same metadata
+			await this.tokenStorage.storeTokens(serverUrl, newTokens, {
+				tokenEndpoint: tokenData.tokenEndpoint,
+				clientId: tokenData.clientId,
+				clientSecret: tokenData.clientSecret,
+			})
+
+			console.log(`[McpOAuthService] Successfully refreshed access token for ${serverUrl}`)
+			return newTokens
+		} catch (error) {
+			console.error(`[McpOAuthService] Error refreshing access token for ${serverUrl}:`, error)
+			return null
+		}
+	}
+
+	/**
+	 * Performs Dynamic Client Registration (RFC 7591)
+	 * @param registrationEndpoint The registration endpoint URL from auth server metadata
+	 * @param redirectUris The redirect URIs to register
+	 * @returns The registered client credentials
+	 */
+	private async registerClient(
+		registrationEndpoint: string,
+		redirectUris: string[],
+	): Promise<RegisteredClientCredentials> {
+		console.log(`[McpOAuthService] Performing Dynamic Client Registration at ${registrationEndpoint}`)
+
+		// Client metadata according to RFC 7591
+		const clientMetadata = {
+			client_name: "Kilo Code",
+			client_uri: "https://kilocode.ai",
+			logo_uri: "https://kilocode.ai/logo.png",
+			redirect_uris: redirectUris,
+			grant_types: ["authorization_code"],
+			response_types: ["code"],
+			token_endpoint_auth_method: "none", // Public client (no secret)
+		}
+
+		console.log(`[McpOAuthService] Client Registration Request:`, JSON.stringify(clientMetadata, null, 2))
+
+		const response = await fetch(registrationEndpoint, {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/json",
+				Accept: "application/json",
+			},
+			body: JSON.stringify(clientMetadata),
+		})
+
+		if (!response.ok) {
+			const text = await response.text()
+			console.error(
+				`[McpOAuthService] Dynamic Client Registration failed: ${response.status} ${response.statusText}`,
+				text,
+			)
+			throw new Error(`Dynamic Client Registration failed: ${response.status} ${response.statusText} - ${text}`)
+		}
+
+		const data: unknown = await response.json()
+		console.log(`[McpOAuthService] Client Registration Response:`, JSON.stringify(data, null, 2))
+
+		const result = ClientRegistrationResponseSchema.safeParse(data)
+
+		if (!result.success) {
+			console.error(`[McpOAuthService] Invalid client registration response:`, result.error.message)
+			throw new Error(`Invalid client registration response: ${result.error.message}`)
+		}
+
+		console.log(`[McpOAuthService] Successfully registered client: ${result.data.client_id}`)
+		console.log(
+			`[McpOAuthService] Registered redirect_uris: ${result.data.redirect_uris?.join(", ") || "not echoed"}`,
+		)
+
+		return {
+			clientId: result.data.client_id,
+			clientSecret: result.data.client_secret,
+			clientIdIssuedAt: result.data.client_id_issued_at,
+			clientSecretExpiresAt: result.data.client_secret_expires_at,
+			redirectUris: result.data.redirect_uris || redirectUris,
+		}
+	}
+
+	/**
+	 * Gets or obtains client credentials for an authorization server.
+	 * Priority:
+	 * 1. Use pre-configured client_id from options
+	 * 2. Use stored registered client credentials
+	 * 3. Perform Dynamic Client Registration if available
+	 * 4. Fall back to Client ID Metadata Document URL
+	 */
+	private async getOrRegisterClient(
+		authServerMetadata: AuthorizationServerMetadata,
+		redirectUris: string[],
+		options?: OAuthConfigOptions,
+	): Promise<{ clientId: string; clientSecret?: string }> {
+		console.log(`[McpOAuthService] getOrRegisterClient for issuer: ${authServerMetadata.issuer}`)
+		console.log(`[McpOAuthService] Redirect URIs to use: ${redirectUris.join(", ")}`)
+
+		// 1. Use pre-configured client_id if provided
+		if (options?.clientId) {
+			console.log("[McpOAuthService] Using pre-configured client_id:", options.clientId)
+			return {
+				clientId: options.clientId,
+				clientSecret: options.clientSecret,
+			}
+		}
+
+		// 2. Check for stored registered credentials
+		const storedCredentials = await this.getStoredClientCredentials(authServerMetadata.issuer)
+		if (storedCredentials) {
+			console.log("[McpOAuthService] Found stored client credentials:", storedCredentials.clientId)
+
+			// Check if redirect URIs match
+			// If stored credentials don't have redirectUris (legacy), we assume they might be stale if we are strict
+			// But to be safe, if we have stored credentials but they don't match what we want now, we should re-register
+			const urisMatch = this.areRedirectUrisEqual(storedCredentials.redirectUris, redirectUris)
+
+			if (!urisMatch) {
+				console.log(
+					"[McpOAuthService] Stored client credentials have different redirect URIs, will re-register",
+				)
+			} else if (
+				!storedCredentials.clientSecretExpiresAt ||
+				storedCredentials.clientSecretExpiresAt > Math.floor(Date.now() / 1000)
+			) {
+				console.log("[McpOAuthService] Using stored registered client credentials:", storedCredentials.clientId)
+				return {
+					clientId: storedCredentials.clientId,
+					clientSecret: storedCredentials.clientSecret,
+				}
+			} else {
+				console.log("[McpOAuthService] Stored client credentials have expired, will re-register")
+			}
+		} else {
+			console.log("[McpOAuthService] No stored client credentials found")
+		}
+
+		// 3. Try Dynamic Client Registration if available
+		if (authServerMetadata.registration_endpoint) {
+			try {
+				const credentials = await this.registerClient(authServerMetadata.registration_endpoint, redirectUris)
+				// Store the registered credentials
+				await this.storeClientCredentials(authServerMetadata.issuer, credentials)
+				return {
+					clientId: credentials.clientId,
+					clientSecret: credentials.clientSecret,
+				}
+			} catch (error) {
+				console.warn(
+					`[McpOAuthService] Dynamic Client Registration failed, falling back to Client ID Metadata Document: ${error}`,
+				)
+			}
+		}
+
+		// 4. Fall back to Client ID Metadata Document URL
+		console.log("[McpOAuthService] Using Client ID Metadata Document URL as client_id")
+		return {
+			clientId: "https://kilocode.ai/.well-known/oauth-client/vscode-extension.json",
+		}
+	}
+
+	/**
+	 * Checks if two sets of redirect URIs are equal (ignoring order)
+	 */
+	private areRedirectUrisEqual(stored?: string[], requested?: string[]): boolean {
+		if (!stored || !requested) {
+			return false
+		}
+		if (stored.length !== requested.length) {
+			return false
+		}
+		const sortedStored = [...stored].sort()
+		const sortedRequested = [...requested].sort()
+		for (let i = 0; i < sortedStored.length; i++) {
+			if (sortedStored[i] !== sortedRequested[i]) {
+				return false
+			}
+		}
+		return true
+	}
+
+	/**
+	 * Probes the MCP server to get the WWW-Authenticate header from a 401 response.
+	 * This is necessary because the MCP SDK doesn't preserve HTTP headers in errors.
+	 * @param serverUrl The MCP server URL to probe
+	 * @returns The WWW-Authenticate header value, or undefined if not available
+	 */
+	private async probeServerForAuthHeader(serverUrl: string): Promise<string | undefined> {
+		try {
+			// Make a GET request to the server URL to trigger a 401 response
+			const response = await fetch(serverUrl, {
+				method: "GET",
+				headers: {
+					Accept: "application/json",
+				},
+			})
+
+			if (response.status === 401) {
+				// Extract the WWW-Authenticate header
+				const authHeader = response.headers.get("WWW-Authenticate") || response.headers.get("www-authenticate")
+				if (authHeader) {
+					console.log(`[McpOAuthService] Got WWW-Authenticate header from ${serverUrl}: ${authHeader}`)
+					return authHeader
+				}
+			}
+
+			console.log(
+				`[McpOAuthService] Probe to ${serverUrl} returned status ${response.status}, no WWW-Authenticate header found`,
+			)
+			return undefined
+		} catch (error) {
+			console.error(`[McpOAuthService] Failed to probe server ${serverUrl} for auth header:`, error)
+			return undefined
+		}
+	}
+
+	private async exchangeCodeForToken(
+		tokenEndpoint: string,
+		code: string,
+		codeVerifier: string,
+		clientId: string,
+		redirectUri: string,
+		clientSecret?: string,
+	): Promise<OAuthTokens> {
+		const body = new URLSearchParams({
+			grant_type: "authorization_code",
+			code,
+			redirect_uri: redirectUri,
+			client_id: clientId,
+			code_verifier: codeVerifier,
+		})
+
+		// If client_secret was provided (from Dynamic Client Registration), include it
+		if (clientSecret) {
+			body.set("client_secret", clientSecret)
+		}
+
+		const response = await fetch(tokenEndpoint, {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/x-www-form-urlencoded",
+			},
+			body: body.toString(),
+		})
+
+		if (!response.ok) {
+			const text = await response.text()
+			throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${text}`)
+		}
+
+		const data = (await response.json()) as any
+
+		// Validate response
+		if (!data.access_token || !data.token_type) {
+			throw new Error("Invalid token response")
+		}
+
+		return {
+			accessToken: data.access_token,
+			tokenType: data.token_type,
+			expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined,
+			refreshToken: data.refresh_token,
+			scope: data.scope,
+		}
+	}
+}

+ 134 - 0
src/services/mcp/oauth/McpOAuthTokenStorage.ts

@@ -0,0 +1,134 @@
+import * as vscode from "vscode"
+import * as crypto from "crypto"
+
+export interface OAuthTokens {
+	accessToken: string
+	tokenType: string
+	expiresAt?: number
+	refreshToken?: string
+	scope?: string
+}
+
+/**
+ * Extended token data that includes authorization server metadata needed for refresh
+ */
+export interface StoredTokenData extends OAuthTokens {
+	serverUrl: string
+	issuedAt: number
+	/** The authorization server's token endpoint URL (needed for refresh) */
+	tokenEndpoint?: string
+	/** The client ID used for this token (needed for refresh) */
+	clientId?: string
+	/** The client secret if available (needed for refresh for confidential clients) */
+	clientSecret?: string
+}
+
+export class McpOAuthTokenStorage {
+	private static readonly SERVER_LIST_KEY = "mcp-oauth-servers-list"
+
+	constructor(private context: vscode.ExtensionContext) {}
+
+	private hashServerUrl(serverUrl: string): string {
+		return crypto.createHash("sha256").update(serverUrl).digest("hex")
+	}
+
+	private getStorageKey(serverUrl: string): string {
+		return `mcp-oauth-${this.hashServerUrl(serverUrl)}`
+	}
+
+	/**
+	 * Stores tokens securely using VS Code SecretStorage
+	 * @param serverUrl The MCP server URL
+	 * @param tokens The OAuth tokens to store
+	 * @param metadata Optional metadata needed for token refresh (token endpoint, client credentials)
+	 */
+	async storeTokens(
+		serverUrl: string,
+		tokens: OAuthTokens,
+		metadata?: {
+			tokenEndpoint?: string
+			clientId?: string
+			clientSecret?: string
+		},
+	): Promise<void> {
+		const data: StoredTokenData = {
+			...tokens,
+			serverUrl,
+			issuedAt: Date.now(),
+			tokenEndpoint: metadata?.tokenEndpoint,
+			clientId: metadata?.clientId,
+			clientSecret: metadata?.clientSecret,
+		}
+		await this.context.secrets.store(this.getStorageKey(serverUrl), JSON.stringify(data))
+		await this.addServerToList(serverUrl)
+	}
+
+	/**
+	 * Retrieves stored tokens (without refresh metadata)
+	 */
+	async getTokens(serverUrl: string): Promise<OAuthTokens | null> {
+		const json = await this.context.secrets.get(this.getStorageKey(serverUrl))
+		if (!json) return null
+		try {
+			const data = JSON.parse(json) as StoredTokenData
+			// Return only the OAuthTokens part
+			return {
+				accessToken: data.accessToken,
+				tokenType: data.tokenType,
+				expiresAt: data.expiresAt,
+				refreshToken: data.refreshToken,
+				scope: data.scope,
+			}
+		} catch (e) {
+			console.error(`Failed to parse stored tokens for ${serverUrl}`, e)
+			return null
+		}
+	}
+
+	/**
+	 * Retrieves the full stored token data including refresh metadata
+	 * This is used by the OAuth service to perform token refresh
+	 */
+	async getFullTokenData(serverUrl: string): Promise<StoredTokenData | null> {
+		const json = await this.context.secrets.get(this.getStorageKey(serverUrl))
+		if (!json) return null
+		try {
+			return JSON.parse(json) as StoredTokenData
+		} catch (e) {
+			console.error(`Failed to parse stored token data for ${serverUrl}`, e)
+			return null
+		}
+	}
+
+	/**
+	 * Removes stored tokens
+	 */
+	async removeTokens(serverUrl: string): Promise<void> {
+		await this.context.secrets.delete(this.getStorageKey(serverUrl))
+		await this.removeServerFromList(serverUrl)
+	}
+
+	/**
+	 * Lists all servers with stored tokens
+	 */
+	async listServers(): Promise<string[]> {
+		return this.context.globalState.get<string[]>(McpOAuthTokenStorage.SERVER_LIST_KEY, [])
+	}
+
+	private async addServerToList(serverUrl: string): Promise<void> {
+		const servers = await this.listServers()
+		if (!servers.includes(serverUrl)) {
+			servers.push(serverUrl)
+			await this.context.globalState.update(McpOAuthTokenStorage.SERVER_LIST_KEY, servers)
+		}
+	}
+
+	private async removeServerFromList(serverUrl: string): Promise<void> {
+		const servers = await this.listServers()
+		const index = servers.indexOf(serverUrl)
+		if (index !== -1) {
+			servers.splice(index, 1)
+			await this.context.globalState.update(McpOAuthTokenStorage.SERVER_LIST_KEY, servers)
+		}
+	}
+}

+ 224 - 0
src/services/mcp/oauth/__tests__/McpOAuthTokenStorage.spec.ts

@@ -0,0 +1,224 @@
+// kilocode_change - new file
+/**
+ * Unit tests for MCP OAuth Token Storage
+ */
+
+import * as vscode from "vscode"
+import { McpOAuthTokenStorage, OAuthTokens } from "../McpOAuthTokenStorage"
+
+// Mock VS Code SecretStorage
+const mockSecretStorage = {
+	get: vi.fn(),
+	store: vi.fn(),
+	delete: vi.fn(),
+	onDidChange: vi.fn(),
+}
+
+// Mock VS Code GlobalState
+const mockGlobalState = {
+	get: vi.fn(),
+	update: vi.fn(),
+	keys: vi.fn(),
+}
+
+// Mock VS Code ExtensionContext
+const mockContext = {
+	secrets: mockSecretStorage,
+	globalState: mockGlobalState,
+} as unknown as vscode.ExtensionContext
+
+describe("McpOAuthTokenStorage", () => {
+	let storage: McpOAuthTokenStorage
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+		mockGlobalState.get.mockReturnValue([])
+		mockGlobalState.update.mockResolvedValue(undefined)
+		storage = new McpOAuthTokenStorage(mockContext)
+	})
+
+	describe("storeTokens", () => {
+		it("should store tokens with hashed server URL key", async () => {
+			const serverUrl = "https://example.com/mcp"
+			const tokens: OAuthTokens = {
+				accessToken: "access-token-123",
+				tokenType: "Bearer",
+				expiresAt: Date.now() + 3600000,
+				refreshToken: "refresh-token-456",
+				scope: "read write",
+			}
+
+			mockSecretStorage.store.mockResolvedValue(undefined)
+
+			await storage.storeTokens(serverUrl, tokens)
+
+			expect(mockSecretStorage.store).toHaveBeenCalledTimes(1)
+			const [key, value] = mockSecretStorage.store.mock.calls[0]
+			expect(key).toMatch(/^mcp-oauth-/)
+			// Value should include the original tokens plus serverUrl and issuedAt
+			const storedData = JSON.parse(value)
+			expect(storedData.accessToken).toBe(tokens.accessToken)
+			expect(storedData.tokenType).toBe(tokens.tokenType)
+			expect(storedData.serverUrl).toBe(serverUrl)
+			expect(storedData.issuedAt).toBeDefined()
+		})
+
+		it("should generate consistent keys for the same server URL", async () => {
+			const serverUrl = "https://example.com/mcp"
+			const tokens: OAuthTokens = {
+				accessToken: "token",
+				tokenType: "Bearer",
+			}
+
+			mockSecretStorage.store.mockResolvedValue(undefined)
+
+			await storage.storeTokens(serverUrl, tokens)
+			const call1Key = mockSecretStorage.store.mock.calls[0][0]
+
+			vi.clearAllMocks()
+			mockGlobalState.get.mockReturnValue([serverUrl])
+			storage = new McpOAuthTokenStorage(mockContext)
+
+			await storage.storeTokens(serverUrl, tokens)
+			const call2Key = mockSecretStorage.store.mock.calls[0][0]
+
+			expect(call1Key).toBe(call2Key)
+		})
+
+		it("should add server URL to server list", async () => {
+			const serverUrl = "https://example.com/mcp"
+			const tokens: OAuthTokens = {
+				accessToken: "token",
+				tokenType: "Bearer",
+			}
+
+			mockGlobalState.get.mockReturnValue([])
+			mockSecretStorage.store.mockResolvedValue(undefined)
+
+			await storage.storeTokens(serverUrl, tokens)
+
+			expect(mockGlobalState.update).toHaveBeenCalledWith("mcp-oauth-servers-list", [serverUrl])
+		})
+	})
+
+	describe("getTokens", () => {
+		it("should retrieve tokens for a server URL", async () => {
+			const serverUrl = "https://example.com/mcp"
+			const storedData = {
+				accessToken: "access-token-123",
+				tokenType: "Bearer",
+				expiresAt: Date.now() + 3600000,
+				serverUrl,
+				issuedAt: Date.now(),
+			}
+
+			mockSecretStorage.get.mockResolvedValue(JSON.stringify(storedData))
+
+			const result = await storage.getTokens(serverUrl)
+
+			expect(mockSecretStorage.get).toHaveBeenCalledTimes(1)
+			expect(result).toEqual({
+				accessToken: storedData.accessToken,
+				tokenType: storedData.tokenType,
+				expiresAt: storedData.expiresAt,
+			})
+		})
+
+		it("should return null when no tokens are stored", async () => {
+			const serverUrl = "https://example.com/mcp"
+
+			mockSecretStorage.get.mockResolvedValue(undefined)
+
+			const result = await storage.getTokens(serverUrl)
+
+			expect(result).toBeNull()
+		})
+
+		it("should return null for invalid JSON", async () => {
+			const serverUrl = "https://example.com/mcp"
+
+			mockSecretStorage.get.mockResolvedValue("invalid-json")
+
+			const result = await storage.getTokens(serverUrl)
+
+			expect(result).toBeNull()
+		})
+
+		it("should strip serverUrl and issuedAt from returned tokens", async () => {
+			const serverUrl = "https://example.com/mcp"
+			const storedData = {
+				accessToken: "token",
+				tokenType: "Bearer",
+				refreshToken: "refresh",
+				scope: "read",
+				serverUrl,
+				issuedAt: Date.now(),
+			}
+
+			mockSecretStorage.get.mockResolvedValue(JSON.stringify(storedData))
+
+			const result = await storage.getTokens(serverUrl)
+
+			expect(result).toBeDefined()
+			expect(result).not.toHaveProperty("serverUrl")
+			expect(result).not.toHaveProperty("issuedAt")
+			expect(result?.accessToken).toBe("token")
+			expect(result?.refreshToken).toBe("refresh")
+		})
+	})
+
+	describe("removeTokens", () => {
+		it("should remove tokens for a server URL", async () => {
+			const serverUrl = "https://example.com/mcp"
+
+			mockGlobalState.get.mockReturnValue([serverUrl])
+			mockSecretStorage.delete.mockResolvedValue(undefined)
+
+			await storage.removeTokens(serverUrl)
+
+			expect(mockSecretStorage.delete).toHaveBeenCalledTimes(1)
+			const [key] = mockSecretStorage.delete.mock.calls[0]
+			expect(key).toMatch(/^mcp-oauth-/)
+		})
+
+		it("should remove server URL from server list", async () => {
+			const serverUrl = "https://example.com/mcp"
+
+			mockGlobalState.get.mockReturnValue([serverUrl, "https://other.com"])
+			mockSecretStorage.delete.mockResolvedValue(undefined)
+
+			await storage.removeTokens(serverUrl)
+
+			expect(mockGlobalState.update).toHaveBeenCalledWith("mcp-oauth-servers-list", ["https://other.com"])
+		})
+	})
+
+	describe("listServers", () => {
+		it("should return empty array when no servers are stored", async () => {
+			mockGlobalState.get.mockReturnValue([])
+
+			const result = await storage.listServers()
+
+			expect(result).toEqual([])
+		})
+
+		it("should return list of server URLs", async () => {
+			const servers = ["https://example1.com/mcp", "https://example2.com/mcp"]
+
+			mockGlobalState.get.mockReturnValue(servers)
+
+			const result = await storage.listServers()
+
+			expect(result).toEqual(servers)
+		})
+
+		it("should use default empty array if globalState returns undefined", async () => {
+			mockGlobalState.get.mockReturnValue(undefined)
+
+			const result = await storage.listServers()
+
+			// The implementation should use default value of []
+			expect(result).toBeUndefined() // Based on implementation, it passes default to get()
+		})
+	})
+})

+ 140 - 0
src/services/mcp/oauth/__tests__/utils.spec.ts

@@ -0,0 +1,140 @@
+// kilocode_change - new file
+/**
+ * Unit tests for MCP OAuth utilities
+ */
+
+import { generateCodeVerifier, generateCodeChallenge, generateState, parseWwwAuthenticateHeader } from "../utils"
+
+describe("MCP OAuth Utils", () => {
+	describe("generateCodeVerifier", () => {
+		it("should generate a string of the expected length", () => {
+			const verifier = generateCodeVerifier()
+			// 32 bytes base64url encoded = 43 characters
+			expect(verifier.length).toBe(43)
+		})
+
+		it("should generate unique verifiers", () => {
+			const verifier1 = generateCodeVerifier()
+			const verifier2 = generateCodeVerifier()
+			expect(verifier1).not.toBe(verifier2)
+		})
+
+		it("should only contain base64url-safe characters", () => {
+			const verifier = generateCodeVerifier()
+			// Base64url uses A-Z, a-z, 0-9, -, _
+			expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/)
+		})
+	})
+
+	describe("generateCodeChallenge", () => {
+		it("should generate a valid S256 challenge from a verifier", () => {
+			const verifier = generateCodeVerifier()
+			const challenge = generateCodeChallenge(verifier)
+
+			// SHA-256 hash base64url encoded = 43 characters
+			expect(challenge.length).toBe(43)
+		})
+
+		it("should generate different challenges for different verifiers", () => {
+			const verifier1 = generateCodeVerifier()
+			const verifier2 = generateCodeVerifier()
+
+			const challenge1 = generateCodeChallenge(verifier1)
+			const challenge2 = generateCodeChallenge(verifier2)
+
+			expect(challenge1).not.toBe(challenge2)
+		})
+
+		it("should generate consistent challenge for the same verifier", () => {
+			const verifier = "test-verifier-12345"
+			const challenge1 = generateCodeChallenge(verifier)
+			const challenge2 = generateCodeChallenge(verifier)
+
+			expect(challenge1).toBe(challenge2)
+		})
+
+		it("should only contain base64url-safe characters", () => {
+			const verifier = generateCodeVerifier()
+			const challenge = generateCodeChallenge(verifier)
+			// Base64url uses A-Z, a-z, 0-9, -, _
+			expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/)
+		})
+	})
+
+	describe("generateState", () => {
+		it("should generate a string of the expected length", () => {
+			const state = generateState()
+			// 32 bytes base64url encoded = 43 characters
+			expect(state.length).toBe(43)
+		})
+
+		it("should generate unique states", () => {
+			const state1 = generateState()
+			const state2 = generateState()
+			expect(state1).not.toBe(state2)
+		})
+
+		it("should only contain base64url-safe characters", () => {
+			const state = generateState()
+			// Base64url uses A-Z, a-z, 0-9, -, _
+			expect(state).toMatch(/^[A-Za-z0-9_-]+$/)
+		})
+	})
+
+	describe("parseWwwAuthenticateHeader", () => {
+		it("should extract resource_metadata URL from Bearer challenge", () => {
+			const header = 'Bearer realm="example", resource_metadata="https://example.com/.well-known/resource"'
+			const result = parseWwwAuthenticateHeader(header)
+
+			expect(result).toBe("https://example.com/.well-known/resource")
+		})
+
+		it("should extract resource_metadata URL without quotes", () => {
+			const header = "Bearer realm=api, resource_metadata=https://example.com/.well-known/resource"
+			const result = parseWwwAuthenticateHeader(header)
+
+			expect(result).toBe("https://example.com/.well-known/resource")
+		})
+
+		it("should return null when no resource_metadata is present", () => {
+			const header = 'Bearer realm="api", error="invalid_token"'
+			const result = parseWwwAuthenticateHeader(header)
+
+			expect(result).toBeNull()
+		})
+
+		it("should return null for empty header", () => {
+			const result = parseWwwAuthenticateHeader("")
+
+			expect(result).toBeNull()
+		})
+
+		it("should return null for header with only scheme", () => {
+			const result = parseWwwAuthenticateHeader("Bearer")
+
+			expect(result).toBeNull()
+		})
+
+		it("should handle complex resource_metadata URLs", () => {
+			const header =
+				'Bearer resource_metadata="https://api.example.com/oauth/.well-known/oauth-protected-resource"'
+			const result = parseWwwAuthenticateHeader(header)
+
+			expect(result).toBe("https://api.example.com/oauth/.well-known/oauth-protected-resource")
+		})
+
+		it("should extract resource_metadata when it appears first", () => {
+			const header = 'Bearer resource_metadata="https://example.com/resource", realm="api"'
+			const result = parseWwwAuthenticateHeader(header)
+
+			expect(result).toBe("https://example.com/resource")
+		})
+
+		it("should handle URLs with query parameters", () => {
+			const header = 'Bearer resource_metadata="https://example.com/.well-known/resource?version=1"'
+			const result = parseWwwAuthenticateHeader(header)
+
+			expect(result).toBe("https://example.com/.well-known/resource?version=1")
+		})
+	})
+})

+ 5 - 0
src/services/mcp/oauth/index.ts

@@ -0,0 +1,5 @@
+export * from "./McpOAuthService"
+export * from "./McpOAuthTokenStorage"
+export * from "./McpAuthorizationDiscovery"
+export * from "./McpOAuthBrowserFlow"
+export * from "./utils"

+ 36 - 0
src/services/mcp/oauth/utils.ts

@@ -0,0 +1,36 @@
+import * as crypto from "crypto"
+
+/**
+ * Generates a cryptographically random PKCE code verifier
+ * Must be 43-128 characters long using unreserved characters
+ */
+export function generateCodeVerifier(): string {
+	// Generate 32 random bytes and encode as base64url (will be 43 characters)
+	const buffer = crypto.randomBytes(32)
+	return buffer.toString("base64url")
+}
+
+/**
+ * Generates the PKCE code challenge from the verifier using S256 method
+ */
+export function generateCodeChallenge(verifier: string): string {
+	const hash = crypto.createHash("sha256").update(verifier).digest()
+	return hash.toString("base64url")
+}
+
+/**
+ * Generates a random state parameter for CSRF protection
+ */
+export function generateState(): string {
+	return crypto.randomBytes(32).toString("base64url")
+}
+
+/**
+ * Parses the WWW-Authenticate header to extract the resource metadata URL
+ * Header format example: Bearer realm="example", resource_metadata="https://..."
+ */
+export function parseWwwAuthenticateHeader(header: string): string | null {
+	// Look for resource_metadata="url" or resource_metadata=url
+	const match = header.match(/resource_metadata="?([^",\s]+)"?/)
+	return match ? match[1] : null
+}

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -111,6 +111,7 @@ export interface WebviewMessage {
 		| "openMcpSettings"
 		| "openProjectMcpSettings"
 		| "restartMcpServer"
+		| "mcpServerOAuthSignIn" // kilocode_change: MCP OAuth sign-in
 		| "refreshAllMcpServers"
 		| "toggleToolAlwaysAllow"
 		| "toggleToolEnabledForPrompt"

+ 42 - 0
src/shared/mcp.ts

@@ -4,6 +4,44 @@ export type McpErrorEntry = {
 	level: "error" | "warn" | "info"
 }
 
+// kilocode_change start - MCP OAuth Authorization
+/**
+ * OAuth authentication status for MCP servers
+ */
+export type McpAuthStatus = {
+	/** Whether the server uses OAuth authentication */
+	method: "oauth" | "static" | "none"
+	/** Current authentication status */
+	status: "authenticated" | "expired" | "required" | "none"
+	/** Token expiry timestamp (Unix milliseconds) */
+	expiresAt?: number
+	/** OAuth scopes granted */
+	scopes?: string[]
+	/** Debug information for OAuth tokens */
+	debug?: McpAuthDebugInfo
+}
+
+/**
+ * Debug information about OAuth token state
+ */
+export type McpAuthDebugInfo = {
+	/** When the token was originally issued (Unix milliseconds) */
+	issuedAt?: number
+	/** Whether the server supports refresh tokens */
+	hasRefreshToken?: boolean
+	/** When the last token refresh occurred (Unix milliseconds) */
+	lastRefreshAt?: number
+	/** When the next token refresh is expected (Unix milliseconds) */
+	nextRefreshAt?: number
+	/** The token endpoint URL used for refresh */
+	tokenEndpoint?: string
+	/** The client ID used for authentication */
+	clientId?: string
+	/** Whether all required metadata for token refresh is available */
+	canRefresh?: boolean
+}
+// kilocode_change end
+
 export type McpServer = {
 	name: string
 	config: string
@@ -18,6 +56,10 @@ export type McpServer = {
 	source?: "global" | "project"
 	projectPath?: string
 	instructions?: string
+	// kilocode_change start - MCP OAuth Authorization
+	/** OAuth authentication status for HTTP-based transports */
+	authStatus?: McpAuthStatus
+	// kilocode_change end
 }
 
 export type McpTool = {

+ 298 - 40
webview-ui/src/components/mcp/McpView.tsx

@@ -1,14 +1,8 @@
 import React, { useState } from "react"
 import { Trans } from "react-i18next"
-import {
-	VSCodeCheckbox,
-	VSCodeLink,
-	VSCodePanels,
-	VSCodePanelTab,
-	VSCodePanelView,
-} from "@vscode/webview-ui-toolkit/react"
+import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
 
-import { McpServer } from "@roo/mcp"
+import { McpServer, McpAuthStatus } from "@roo/mcp"
 
 import { vscode } from "@src/utils/vscode"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
@@ -22,6 +16,10 @@ import {
 	DialogDescription,
 	DialogFooter,
 	ToggleSwitch,
+	Tabs,
+	TabsList,
+	TabsTrigger,
+	TabsContent,
 	// StandardTooltip, // kilocode_change: not used
 } from "@src/components/ui"
 import { buildDocLink } from "@src/utils/docLinks"
@@ -262,6 +260,16 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
 		})
 	}
 
+	// kilocode_change start: OAuth sign-in handler
+	const handleOAuthSignIn = () => {
+		vscode.postMessage({
+			type: "mcpServerOAuthSignIn",
+			serverName: server.name,
+			source: server.source || "global",
+		})
+	}
+	// kilocode_change end
+
 	const handleTimeoutChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
 		const seconds = parseInt(event.target.value)
 		setTimeoutValue(seconds)
@@ -371,22 +379,32 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
 								fontSize: "13px",
 								borderRadius: "0 0 4px 4px",
 							}}>
-							<VSCodePanels style={{ marginBottom: "10px" }}>
-								<VSCodePanelTab id="tools">
-									{t("mcp:tabs.tools")} ({server.tools?.length || 0})
-								</VSCodePanelTab>
-								<VSCodePanelTab id="resources">
-									{t("mcp:tabs.resources")} (
-									{[...(server.resourceTemplates || []), ...(server.resources || [])].length || 0})
-								</VSCodePanelTab>
-								{server.instructions && (
-									<VSCodePanelTab id="instructions">{t("mcp:instructions")}</VSCodePanelTab>
-								)}
-								<VSCodePanelTab id="logs">
-									{t("mcp:tabs.logs")} ({server.errorHistory?.length || 0})
-								</VSCodePanelTab>
+							{/* kilocode_change start: Replace VSCodePanels with Radix UI Tabs for independent tab overflow */}
+							<Tabs defaultValue="tools" style={{ marginBottom: "10px" }}>
+								<div className="overflow-x-auto scrollbar-hide">
+									<TabsList className="w-max min-w-full">
+										<TabsTrigger value="tools">
+											{t("mcp:tabs.tools")} ({server.tools?.length || 0})
+										</TabsTrigger>
+										<TabsTrigger value="resources">
+											{t("mcp:tabs.resources")} (
+											{[...(server.resourceTemplates || []), ...(server.resources || [])]
+												.length || 0}
+											)
+										</TabsTrigger>
+										{server.instructions && (
+											<TabsTrigger value="instructions">{t("mcp:instructions")}</TabsTrigger>
+										)}
+										<TabsTrigger value="logs">
+											{t("mcp:tabs.logs")} ({server.errorHistory?.length || 0})
+										</TabsTrigger>
+										{server.authStatus?.method === "oauth" && (
+											<TabsTrigger value="auth-debug">Auth</TabsTrigger>
+										)}
+									</TabsList>
+								</div>
 
-								<VSCodePanelView id="tools-view">
+								<TabsContent value="tools">
 									{server.tools && server.tools.length > 0 ? (
 										<div
 											style={{
@@ -411,9 +429,9 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
 											{t("mcp:emptyState.noTools")}
 										</div>
 									)}
-								</VSCodePanelView>
+								</TabsContent>
 
-								<VSCodePanelView id="resources-view">
+								<TabsContent value="resources">
 									{(server.resources && server.resources.length > 0) ||
 									(server.resourceTemplates && server.resourceTemplates.length > 0) ? (
 										<div
@@ -438,19 +456,19 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
 											{t("mcp:emptyState.noResources")}
 										</div>
 									)}
-								</VSCodePanelView>
+								</TabsContent>
 
 								{server.instructions && (
-									<VSCodePanelView id="instructions-view">
+									<TabsContent value="instructions">
 										<div style={{ padding: "10px 0", fontSize: "12px" }}>
 											<div className="opacity-80 whitespace-pre-wrap break-words">
 												{server.instructions}
 											</div>
 										</div>
-									</VSCodePanelView>
+									</TabsContent>
 								)}
 
-								<VSCodePanelView id="logs-view">
+								<TabsContent value="logs">
 									{server.errorHistory && server.errorHistory.length > 0 ? (
 										<div
 											style={{
@@ -471,8 +489,15 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
 											{t("mcp:emptyState.noLogs")}
 										</div>
 									)}
-								</VSCodePanelView>
-							</VSCodePanels>
+								</TabsContent>
+
+								{server.authStatus?.method === "oauth" && (
+									<TabsContent value="auth-debug">
+										<OAuthDebugInfo authStatus={server.authStatus} />
+									</TabsContent>
+								)}
+							</Tabs>
+							{/* kilocode_change end */}
 
 							{/* Network Timeout */}
 							<div style={{ padding: "10px 7px" }}>
@@ -540,15 +565,30 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
 										</React.Fragment>
 									))}
 							</div>
-							<Button
-								variant="secondary"
-								onClick={handleRestart}
-								disabled={server.status === "connecting"}
-								style={{ width: "calc(100% - 20px)", margin: "0 10px 10px 10px" }}>
-								{server.status === "connecting"
-									? t("mcp:serverStatus.retrying")
-									: t("mcp:serverStatus.retryConnection")}
-							</Button>
+							{/* kilocode_change start: OAuth sign-in button */}
+							{server.authStatus?.status === "required" ? (
+								<Button
+									variant="secondary"
+									onClick={handleOAuthSignIn}
+									disabled={server.status === "connecting"}
+									style={{ width: "calc(100% - 20px)", margin: "0 10px 10px 10px" }}>
+									<span className="codicon codicon-sign-in" style={{ marginRight: "6px" }}></span>
+									{server.status === "connecting"
+										? t("mcp:serverStatus.signingIn")
+										: t("mcp:serverStatus.signIn")}
+								</Button>
+							) : (
+								<Button
+									variant="secondary"
+									onClick={handleRestart}
+									disabled={server.status === "connecting"}
+									style={{ width: "calc(100% - 20px)", margin: "0 10px 10px 10px" }}>
+									{server.status === "connecting"
+										? t("mcp:serverStatus.retrying")
+										: t("mcp:serverStatus.retryConnection")}
+								</Button>
+							)}
+							{/* kilocode_change end */}
 						</div>
 					)}
 
@@ -575,4 +615,222 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
 	)
 }
 
+// kilocode_change start: OAuth Debug Info Component
+/**
+ * Formats a timestamp to a human-readable date/time string
+ */
+const formatTimestamp = (timestamp?: number): string => {
+	if (!timestamp) return "N/A"
+	return new Date(timestamp).toLocaleString()
+}
+
+/**
+ * Formats a timestamp to a relative time string (e.g., "in 5 minutes", "2 hours ago")
+ */
+const formatRelativeTime = (timestamp?: number): string => {
+	if (!timestamp) return "N/A"
+
+	const now = Date.now()
+	const diff = timestamp - now
+	const absDiff = Math.abs(diff)
+
+	const minutes = Math.floor(absDiff / (1000 * 60))
+	const hours = Math.floor(absDiff / (1000 * 60 * 60))
+	const days = Math.floor(absDiff / (1000 * 60 * 60 * 24))
+
+	if (diff > 0) {
+		// Future
+		if (minutes < 1) return "in less than a minute"
+		if (minutes < 60) return `in ${minutes} minute${minutes === 1 ? "" : "s"}`
+		if (hours < 24) return `in ${hours} hour${hours === 1 ? "" : "s"}`
+		return `in ${days} day${days === 1 ? "" : "s"}`
+	} else {
+		// Past
+		if (minutes < 1) return "just now"
+		if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`
+		if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`
+		return `${days} day${days === 1 ? "" : "s"} ago`
+	}
+}
+
+/**
+ * Component to display OAuth debug information for an MCP server
+ */
+const OAuthDebugInfo = ({ authStatus }: { authStatus: McpAuthStatus }) => {
+	const { t } = useAppTranslation()
+	const debug = authStatus.debug
+
+	const infoRowStyle: React.CSSProperties = {
+		display: "flex",
+		justifyContent: "space-between",
+		padding: "6px 0",
+		borderBottom: "1px solid var(--vscode-widget-border)",
+	}
+
+	const labelStyle: React.CSSProperties = {
+		color: "var(--vscode-descriptionForeground)",
+		fontSize: "12px",
+	}
+
+	const valueStyle: React.CSSProperties = {
+		fontSize: "12px",
+		textAlign: "right",
+		maxWidth: "60%",
+		wordBreak: "break-all",
+	}
+
+	return (
+		<div style={{ padding: "10px 0", width: "100%" }}>
+			<div style={{ marginBottom: "12px" }}>
+				<h4 style={{ margin: "0 0 8px 0", fontSize: "13px" }}>{t("mcp:authDebug.title")}</h4>
+			</div>
+
+			{/* Auth Status */}
+			<div style={infoRowStyle}>
+				<span style={labelStyle}>{t("mcp:authDebug.status")}</span>
+				<span
+					style={{
+						...valueStyle,
+						color:
+							authStatus.status === "authenticated"
+								? "var(--vscode-testing-iconPassed)"
+								: authStatus.status === "expired"
+									? "var(--vscode-testing-iconFailed)"
+									: "var(--vscode-charts-yellow)",
+					}}>
+					{authStatus.status}
+				</span>
+			</div>
+
+			{/* Token Expiration */}
+			<div style={infoRowStyle}>
+				<span style={labelStyle}>{t("mcp:authDebug.tokenExpires")}</span>
+				<span style={valueStyle}>
+					{authStatus.expiresAt ? (
+						<>
+							{formatTimestamp(authStatus.expiresAt)}
+							<br />
+							<span style={{ color: "var(--vscode-descriptionForeground)", fontSize: "11px" }}>
+								({formatRelativeTime(authStatus.expiresAt)})
+							</span>
+						</>
+					) : (
+						"N/A"
+					)}
+				</span>
+			</div>
+
+			{/* Scopes */}
+			{authStatus.scopes && authStatus.scopes.length > 0 && (
+				<div style={infoRowStyle}>
+					<span style={labelStyle}>{t("mcp:authDebug.scopes")}</span>
+					<span style={valueStyle}>{authStatus.scopes.join(", ")}</span>
+				</div>
+			)}
+
+			{/* Debug Info Section */}
+			{debug && (
+				<>
+					<div style={{ margin: "16px 0 8px 0" }}>
+						<h4 style={{ margin: "0", fontSize: "13px" }}>{t("mcp:authDebug.refreshInfo")}</h4>
+					</div>
+
+					{/* Has Refresh Token */}
+					<div style={infoRowStyle}>
+						<span style={labelStyle}>{t("mcp:authDebug.hasRefreshToken")}</span>
+						<span
+							style={{
+								...valueStyle,
+								color: debug.hasRefreshToken
+									? "var(--vscode-testing-iconPassed)"
+									: "var(--vscode-testing-iconFailed)",
+							}}>
+							{debug.hasRefreshToken ? "Yes" : "No"}
+						</span>
+					</div>
+
+					{/* Can Refresh */}
+					<div style={infoRowStyle}>
+						<span style={labelStyle}>{t("mcp:authDebug.canRefresh")}</span>
+						<span
+							style={{
+								...valueStyle,
+								color: debug.canRefresh
+									? "var(--vscode-testing-iconPassed)"
+									: "var(--vscode-testing-iconFailed)",
+							}}>
+							{debug.canRefresh ? "Yes" : "No"}
+						</span>
+					</div>
+
+					{/* Token Issued At */}
+					<div style={infoRowStyle}>
+						<span style={labelStyle}>{t("mcp:authDebug.tokenIssuedAt")}</span>
+						<span style={valueStyle}>
+							{debug.issuedAt ? (
+								<>
+									{formatTimestamp(debug.issuedAt)}
+									<br />
+									<span style={{ color: "var(--vscode-descriptionForeground)", fontSize: "11px" }}>
+										({formatRelativeTime(debug.issuedAt)})
+									</span>
+								</>
+							) : (
+								"N/A"
+							)}
+						</span>
+					</div>
+
+					{/* Next Refresh At */}
+					{debug.nextRefreshAt && (
+						<div style={infoRowStyle}>
+							<span style={labelStyle}>{t("mcp:authDebug.nextRefreshAt")}</span>
+							<span style={valueStyle}>
+								{formatTimestamp(debug.nextRefreshAt)}
+								<br />
+								<span style={{ color: "var(--vscode-descriptionForeground)", fontSize: "11px" }}>
+									({formatRelativeTime(debug.nextRefreshAt)})
+								</span>
+							</span>
+						</div>
+					)}
+
+					{/* Last Refresh At */}
+					{debug.lastRefreshAt && (
+						<div style={infoRowStyle}>
+							<span style={labelStyle}>{t("mcp:authDebug.lastRefreshAt")}</span>
+							<span style={valueStyle}>
+								{formatTimestamp(debug.lastRefreshAt)}
+								<br />
+								<span style={{ color: "var(--vscode-descriptionForeground)", fontSize: "11px" }}>
+									({formatRelativeTime(debug.lastRefreshAt)})
+								</span>
+							</span>
+						</div>
+					)}
+
+					{/* Token Endpoint */}
+					{debug.tokenEndpoint && (
+						<div style={infoRowStyle}>
+							<span style={labelStyle}>{t("mcp:authDebug.tokenEndpoint")}</span>
+							<span style={{ ...valueStyle, fontSize: "11px" }}>{debug.tokenEndpoint}</span>
+						</div>
+					)}
+
+					{/* Client ID */}
+					{debug.clientId && (
+						<div style={infoRowStyle}>
+							<span style={labelStyle}>{t("mcp:authDebug.clientId")}</span>
+							<span style={{ ...valueStyle, fontSize: "11px" }}>
+								{debug.clientId.length > 50 ? `${debug.clientId.substring(0, 50)}...` : debug.clientId}
+							</span>
+						</div>
+					)}
+				</>
+			)}
+		</div>
+	)
+}
+// kilocode_change end
+
 export default McpView

+ 19 - 2
webview-ui/src/i18n/locales/ar/mcp.json

@@ -26,7 +26,8 @@
 	"tabs": {
 		"tools": "الأدوات",
 		"resources": "الموارد",
-		"logs": "السجلات"
+		"logs": "السجلات",
+		"authDebug": "تصحيح المصادقة"
 	},
 	"emptyState": {
 		"noTools": "ما فيه أدوات",
@@ -55,11 +56,27 @@
 	},
 	"serverStatus": {
 		"retrying": "جاري المحاولة من جديد...",
-		"retryConnection": "إعادة محاولة الاتصال"
+		"retryConnection": "إعادة محاولة الاتصال",
+		"signIn": "تسجيل الدخول",
+		"signingIn": "جاري تسجيل الدخول..."
 	},
 	"execution": {
 		"running": "قيد التشغيل",
 		"completed": "اكتمل",
 		"error": "خطأ"
+	},
+	"authDebug": {
+		"title": "معلومات تصحيح OAuth",
+		"status": "الحالة",
+		"tokenExpires": "انتهاء صلاحية Token",
+		"scopes": "الصلاحيات",
+		"refreshInfo": "معلومات Refresh Token",
+		"hasRefreshToken": "يوجد Refresh Token",
+		"canRefresh": "يمكن التحديث التلقائي",
+		"tokenIssuedAt": "وقت إصدار Token",
+		"nextRefreshAt": "موعد التحديث القادم",
+		"lastRefreshAt": "آخر تحديث",
+		"tokenEndpoint": "Token Endpoint",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/ca/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Eines",
 		"resources": "Recursos",
-		"logs": "Registres"
+		"logs": "Registres",
+		"authDebug": "Depuració d'autenticació"
 	},
 	"emptyState": {
 		"noTools": "No s'han trobat eines",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "Tornant a intentar...",
-		"retryConnection": "Torna a intentar la connexió"
+		"retryConnection": "Torna a intentar la connexió",
+		"signIn": "Inicia sessió",
+		"signingIn": "Iniciant sessió..."
 	},
 	"refreshMCP": "Actualitza els servidors MCP",
 	"execution": {
 		"running": "En execució",
 		"completed": "Completat",
 		"error": "Error"
+	},
+	"authDebug": {
+		"title": "Informació de depuració OAuth",
+		"status": "Estat",
+		"tokenExpires": "Caducitat del token",
+		"scopes": "Àmbits",
+		"refreshInfo": "Informació del token d'actualització",
+		"hasRefreshToken": "Té token d'actualització",
+		"canRefresh": "Pot actualitzar automàticament",
+		"tokenIssuedAt": "Token emès el",
+		"nextRefreshAt": "Propera actualització el",
+		"lastRefreshAt": "Última actualització el",
+		"tokenEndpoint": "Punt d'accés del token",
+		"clientId": "ID de client"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/cs/mcp.json

@@ -26,7 +26,8 @@
 	"tabs": {
 		"tools": "Nástroje",
 		"resources": "Zdroje",
-		"logs": "Logy"
+		"logs": "Logy",
+		"authDebug": "Ladění Auth"
 	},
 	"emptyState": {
 		"noTools": "Nenalezeny žádné nástroje",
@@ -55,11 +56,27 @@
 	},
 	"serverStatus": {
 		"retrying": "Opakování...",
-		"retryConnection": "Opakovat připojení"
+		"retryConnection": "Opakovat připojení",
+		"signIn": "Přihlásit se",
+		"signingIn": "Přihlašování..."
 	},
 	"execution": {
 		"running": "Běží",
 		"completed": "Dokončeno",
 		"error": "Chyba"
+	},
+	"authDebug": {
+		"title": "OAuth Ladící informace",
+		"status": "Stav",
+		"tokenExpires": "Token vyprší",
+		"scopes": "Rozsahy",
+		"refreshInfo": "Informace o refresh tokenu",
+		"hasRefreshToken": "Má refresh token",
+		"canRefresh": "Automatické obnovení",
+		"tokenIssuedAt": "Token vydán",
+		"nextRefreshAt": "Další obnovení",
+		"lastRefreshAt": "Poslední obnovení",
+		"tokenEndpoint": "Token endpoint",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/de/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Tools",
 		"resources": "Ressourcen",
-		"logs": "Protokolle"
+		"logs": "Protokolle",
+		"authDebug": "Auth-Debug"
 	},
 	"emptyState": {
 		"noTools": "Keine Tools gefunden",
@@ -54,7 +55,23 @@
 	},
 	"serverStatus": {
 		"retrying": "Wiederhole...",
-		"retryConnection": "Verbindung wiederholen"
+		"retryConnection": "Verbindung wiederholen",
+		"signIn": "Anmelden",
+		"signingIn": "Anmeldung läuft..."
+	},
+	"authDebug": {
+		"title": "OAuth-Debug-Informationen",
+		"status": "Status",
+		"tokenExpires": "Token läuft ab",
+		"scopes": "Scopes",
+		"refreshInfo": "Refresh-Token-Info",
+		"hasRefreshToken": "Hat Refresh-Token",
+		"canRefresh": "Kann automatisch aktualisieren",
+		"tokenIssuedAt": "Token ausgestellt am",
+		"nextRefreshAt": "Nächste Aktualisierung um",
+		"lastRefreshAt": "Letzte Aktualisierung um",
+		"tokenEndpoint": "Token-Endpunkt",
+		"clientId": "Client-ID"
 	},
 	"refreshMCP": "MCP-Server aktualisieren",
 	"execution": {

+ 19 - 2
webview-ui/src/i18n/locales/en/mcp.json

@@ -26,7 +26,8 @@
 	"tabs": {
 		"tools": "Tools",
 		"resources": "Resources",
-		"logs": "Logs"
+		"logs": "Logs",
+		"authDebug": "Auth Debug"
 	},
 	"emptyState": {
 		"noTools": "No tools found",
@@ -55,11 +56,27 @@
 	},
 	"serverStatus": {
 		"retrying": "Retrying...",
-		"retryConnection": "Retry Connection"
+		"retryConnection": "Retry Connection",
+		"signIn": "Sign in",
+		"signingIn": "Signing in..."
 	},
 	"execution": {
 		"running": "Running",
 		"completed": "Completed",
 		"error": "Error"
+	},
+	"authDebug": {
+		"title": "OAuth Authentication Details",
+		"status": "Auth Status",
+		"tokenExpires": "Token Expires",
+		"scopes": "Scopes",
+		"refreshInfo": "Refresh Token Info",
+		"hasRefreshToken": "Has Refresh Token",
+		"canRefresh": "Can Auto-Refresh",
+		"tokenIssuedAt": "Token Issued At",
+		"nextRefreshAt": "Next Auto-Refresh",
+		"lastRefreshAt": "Last Refresh",
+		"tokenEndpoint": "Token Endpoint",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/es/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Herramientas",
 		"resources": "Recursos",
-		"logs": "Registros"
+		"logs": "Registros",
+		"authDebug": "Depuración de Auth"
 	},
 	"emptyState": {
 		"noTools": "No se encontraron herramientas",
@@ -54,7 +55,23 @@
 	},
 	"serverStatus": {
 		"retrying": "Reintentando...",
-		"retryConnection": "Reintentar conexión"
+		"retryConnection": "Reintentar conexión",
+		"signIn": "Iniciar sesión",
+		"signingIn": "Iniciando sesión..."
+	},
+	"authDebug": {
+		"title": "Información de depuración OAuth",
+		"status": "Estado",
+		"tokenExpires": "Token expira",
+		"scopes": "Scopes",
+		"refreshInfo": "Información del token de actualización",
+		"hasRefreshToken": "Tiene token de actualización",
+		"canRefresh": "Puede actualizarse automáticamente",
+		"tokenIssuedAt": "Token emitido el",
+		"nextRefreshAt": "Próxima actualización",
+		"lastRefreshAt": "Última actualización",
+		"tokenEndpoint": "Token Endpoint",
+		"clientId": "Client ID"
 	},
 	"refreshMCP": "Actualizar Servidores MCP",
 	"execution": {

+ 19 - 2
webview-ui/src/i18n/locales/fr/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Outils",
 		"resources": "Ressources",
-		"logs": "Journaux"
+		"logs": "Journaux",
+		"authDebug": "Débogage Auth"
 	},
 	"emptyState": {
 		"noTools": "Aucun outil trouvé",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "Nouvelle tentative...",
-		"retryConnection": "Réessayer la connexion"
+		"retryConnection": "Réessayer la connexion",
+		"signIn": "Se connecter",
+		"signingIn": "Connexion en cours..."
 	},
 	"refreshMCP": "Rafraîchir les serveurs MCP",
 	"execution": {
 		"running": "En cours",
 		"completed": "Terminé",
 		"error": "Erreur"
+	},
+	"authDebug": {
+		"title": "Informations de débogage OAuth",
+		"status": "Statut",
+		"tokenExpires": "Expiration du token",
+		"scopes": "Portées",
+		"refreshInfo": "Informations du token de rafraîchissement",
+		"hasRefreshToken": "A un token de rafraîchissement",
+		"canRefresh": "Peut se rafraîchir automatiquement",
+		"tokenIssuedAt": "Token émis le",
+		"nextRefreshAt": "Prochain rafraîchissement le",
+		"lastRefreshAt": "Dernier rafraîchissement le",
+		"tokenEndpoint": "Point de terminaison du token",
+		"clientId": "ID client"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/hi/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "टूल्स",
 		"resources": "संसाधन",
-		"logs": "लॉग्स"
+		"logs": "लॉग्स",
+		"authDebug": "Auth डीबग"
 	},
 	"emptyState": {
 		"noTools": "कोई टूल नहीं मिला",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "फिर से कोशिश कर रहा है...",
-		"retryConnection": "कनेक्शन फिर से आज़माएँ"
+		"retryConnection": "कनेक्शन फिर से आज़माएँ",
+		"signIn": "साइन इन करें",
+		"signingIn": "साइन इन हो रहा है..."
 	},
 	"refreshMCP": "एमसीपी सर्वर रीफ्रेश करें",
 	"execution": {
 		"running": "चल रहा है",
 		"completed": "पूरा हुआ",
 		"error": "त्रुटि"
+	},
+	"authDebug": {
+		"title": "OAuth डीबग जानकारी",
+		"status": "स्थिति",
+		"tokenExpires": "Token समाप्ति",
+		"scopes": "स्कोप",
+		"refreshInfo": "Refresh Token जानकारी",
+		"hasRefreshToken": "Refresh Token है",
+		"canRefresh": "ऑटो-रिफ्रेश कर सकता है",
+		"tokenIssuedAt": "Token जारी होने का समय",
+		"nextRefreshAt": "अगला रिफ्रेश",
+		"lastRefreshAt": "पिछला रिफ्रेश",
+		"tokenEndpoint": "Token Endpoint",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/id/mcp.json

@@ -26,7 +26,8 @@
 	"tabs": {
 		"tools": "Tools",
 		"resources": "Resources",
-		"logs": "Log"
+		"logs": "Log",
+		"authDebug": "Debug Auth"
 	},
 	"emptyState": {
 		"noTools": "Tidak ada tools ditemukan",
@@ -55,11 +56,27 @@
 	},
 	"serverStatus": {
 		"retrying": "Mencoba lagi...",
-		"retryConnection": "Coba Koneksi Lagi"
+		"retryConnection": "Coba Koneksi Lagi",
+		"signIn": "Masuk",
+		"signingIn": "Sedang Masuk..."
 	},
 	"execution": {
 		"running": "Berjalan",
 		"completed": "Selesai",
 		"error": "Error"
+	},
+	"authDebug": {
+		"title": "Informasi Debug OAuth",
+		"status": "Status",
+		"tokenExpires": "Token Kedaluwarsa",
+		"scopes": "Scopes",
+		"refreshInfo": "Info Refresh Token",
+		"hasRefreshToken": "Punya Refresh Token",
+		"canRefresh": "Bisa Auto-Refresh",
+		"tokenIssuedAt": "Token Diterbitkan Pada",
+		"nextRefreshAt": "Refresh Berikutnya Pada",
+		"lastRefreshAt": "Refresh Terakhir Pada",
+		"tokenEndpoint": "Token Endpoint",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/it/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Strumenti",
 		"resources": "Risorse",
-		"logs": "Registri"
+		"logs": "Registri",
+		"authDebug": "Debug Auth"
 	},
 	"emptyState": {
 		"noTools": "Nessuno strumento trovato",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "Riprovo...",
-		"retryConnection": "Riprova connessione"
+		"retryConnection": "Riprova connessione",
+		"signIn": "Accedi",
+		"signingIn": "Accesso in corso..."
 	},
 	"refreshMCP": "Aggiorna server MCP",
 	"execution": {
 		"running": "In esecuzione",
 		"completed": "Completato",
 		"error": "Errore"
+	},
+	"authDebug": {
+		"title": "Informazioni di debug OAuth",
+		"status": "Stato",
+		"tokenExpires": "Scadenza token",
+		"scopes": "Ambiti",
+		"refreshInfo": "Info refresh token",
+		"hasRefreshToken": "Ha refresh token",
+		"canRefresh": "Auto-refresh disponibile",
+		"tokenIssuedAt": "Token emesso il",
+		"nextRefreshAt": "Prossimo refresh",
+		"lastRefreshAt": "Ultimo refresh",
+		"tokenEndpoint": "Token endpoint",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/ja/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "ツール",
 		"resources": "リソース",
-		"logs": "ログ"
+		"logs": "ログ",
+		"authDebug": "認証デバッグ"
 	},
 	"emptyState": {
 		"noTools": "ツールが見つかりません",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "再試行中...",
-		"retryConnection": "再接続"
+		"retryConnection": "再接続",
+		"signIn": "サインイン",
+		"signingIn": "サインイン中..."
 	},
 	"refreshMCP": "MCPサーバーを更新",
 	"execution": {
 		"running": "実行中",
 		"completed": "完了",
 		"error": "エラー"
+	},
+	"authDebug": {
+		"title": "OAuthデバッグ情報",
+		"status": "ステータス",
+		"tokenExpires": "tokenの有効期限",
+		"scopes": "スコープ",
+		"refreshInfo": "refresh tokenの情報",
+		"hasRefreshToken": "refresh tokenあり",
+		"canRefresh": "自動更新可能",
+		"tokenIssuedAt": "token発行日時",
+		"nextRefreshAt": "次回更新予定",
+		"lastRefreshAt": "最終更新日時",
+		"tokenEndpoint": "tokenエンドポイント",
+		"clientId": "クライアントID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/ko/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "도구",
 		"resources": "리소스",
-		"logs": "로그"
+		"logs": "로그",
+		"authDebug": "인증 디버그"
 	},
 	"emptyState": {
 		"noTools": "도구를 찾을 수 없음",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "다시 시도 중...",
-		"retryConnection": "연결 다시 시도"
+		"retryConnection": "연결 다시 시도",
+		"signIn": "로그인",
+		"signingIn": "로그인 중..."
 	},
 	"refreshMCP": "MCP 서버 새로 고침",
 	"execution": {
 		"running": "실행 중",
 		"completed": "완료됨",
 		"error": "오류"
+	},
+	"authDebug": {
+		"title": "OAuth 디버그 정보",
+		"status": "상태",
+		"tokenExpires": "Token 만료 시간",
+		"scopes": "권한 범위",
+		"refreshInfo": "Refresh Token 정보",
+		"hasRefreshToken": "Refresh Token 보유",
+		"canRefresh": "자동 갱신 가능",
+		"tokenIssuedAt": "Token 발급 시간",
+		"nextRefreshAt": "다음 갱신 시간",
+		"lastRefreshAt": "마지막 갱신 시간",
+		"tokenEndpoint": "Token 엔드포인트",
+		"clientId": "클라이언트 ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/nl/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Tools",
 		"resources": "Bronnen",
-		"logs": "Logboeken"
+		"logs": "Logboeken",
+		"authDebug": "Auth Debug"
 	},
 	"emptyState": {
 		"noTools": "Geen tools gevonden",
@@ -54,7 +55,23 @@
 	},
 	"serverStatus": {
 		"retrying": "Opnieuw proberen...",
-		"retryConnection": "Verbinding opnieuw proberen"
+		"retryConnection": "Verbinding opnieuw proberen",
+		"signIn": "Aanmelden",
+		"signingIn": "Bezig met aanmelden..."
+	},
+	"authDebug": {
+		"title": "OAuth Debug-informatie",
+		"status": "Status",
+		"tokenExpires": "Token verloopt",
+		"scopes": "Scopes",
+		"refreshInfo": "Refresh Token-info",
+		"hasRefreshToken": "Heeft Refresh Token",
+		"canRefresh": "Kan automatisch vernieuwen",
+		"tokenIssuedAt": "Token uitgegeven op",
+		"nextRefreshAt": "Volgende vernieuwing op",
+		"lastRefreshAt": "Laatste vernieuwing op",
+		"tokenEndpoint": "Token Endpoint",
+		"clientId": "Client ID"
 	},
 	"refreshMCP": "MCP-servers vernieuwen",
 	"execution": {

+ 19 - 2
webview-ui/src/i18n/locales/pl/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Narzędzia",
 		"resources": "Zasoby",
-		"logs": "Logi"
+		"logs": "Logi",
+		"authDebug": "Debugowanie uwierzytelniania"
 	},
 	"emptyState": {
 		"noTools": "Nie znaleziono narzędzi",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "Ponawianie...",
-		"retryConnection": "Ponów połączenie"
+		"retryConnection": "Ponów połączenie",
+		"signIn": "Zaloguj się",
+		"signingIn": "Logowanie..."
 	},
 	"refreshMCP": "Odśwież serwery MCP",
 	"execution": {
 		"running": "Uruchomione",
 		"completed": "Zakończone",
 		"error": "Błąd"
+	},
+	"authDebug": {
+		"title": "Informacje debugowania OAuth",
+		"status": "Status",
+		"tokenExpires": "Token wygasa",
+		"scopes": "Zakresy",
+		"refreshInfo": "Informacje o tokenie odświeżania",
+		"hasRefreshToken": "Posiada token odświeżania",
+		"canRefresh": "Może automatycznie odświeżać",
+		"tokenIssuedAt": "Token wydany o",
+		"nextRefreshAt": "Następne odświeżenie o",
+		"lastRefreshAt": "Ostatnie odświeżenie o",
+		"tokenEndpoint": "Punkt końcowy tokenu",
+		"clientId": "ID klienta"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/pt-BR/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Ferramentas",
 		"resources": "Recursos",
-		"logs": "Logs"
+		"logs": "Logs",
+		"authDebug": "Debug de Autenticação"
 	},
 	"emptyState": {
 		"noTools": "Nenhuma ferramenta encontrada",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "Tentando novamente...",
-		"retryConnection": "Tentar reconectar"
+		"retryConnection": "Tentar reconectar",
+		"signIn": "Entrar",
+		"signingIn": "Entrando..."
 	},
 	"refreshMCP": "Atualizar Servidores MCP",
 	"execution": {
 		"running": "Em execução",
 		"completed": "Concluído",
 		"error": "Erro"
+	},
+	"authDebug": {
+		"title": "Informações de Depuração OAuth",
+		"status": "Status",
+		"tokenExpires": "Token Expira",
+		"scopes": "Escopos",
+		"refreshInfo": "Informações do Refresh Token",
+		"hasRefreshToken": "Tem Refresh Token",
+		"canRefresh": "Pode Atualizar Automaticamente",
+		"tokenIssuedAt": "Token Emitido Em",
+		"nextRefreshAt": "Próxima Atualização Em",
+		"lastRefreshAt": "Última Atualização Em",
+		"tokenEndpoint": "Endpoint do Token",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/ru/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Инструменты",
 		"resources": "Ресурсы",
-		"logs": "Логи"
+		"logs": "Логи",
+		"authDebug": "Отладка OAuth"
 	},
 	"emptyState": {
 		"noTools": "Инструменты не найдены",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "Повторная попытка...",
-		"retryConnection": "Повторить подключение"
+		"retryConnection": "Повторить подключение",
+		"signIn": "Войти",
+		"signingIn": "Вход..."
 	},
 	"refreshMCP": "Обновить MCP серверы",
 	"execution": {
 		"running": "Выполняется",
 		"completed": "Завершено",
 		"error": "Ошибка"
+	},
+	"authDebug": {
+		"title": "Отладочная информация OAuth",
+		"status": "Статус",
+		"tokenExpires": "Срок действия токена",
+		"scopes": "Scopes",
+		"refreshInfo": "Информация о Refresh Token",
+		"hasRefreshToken": "Есть Refresh Token",
+		"canRefresh": "Возможно автообновление",
+		"tokenIssuedAt": "Токен выдан",
+		"nextRefreshAt": "Следующее обновление",
+		"lastRefreshAt": "Последнее обновление",
+		"tokenEndpoint": "Token Endpoint",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/th/mcp.json

@@ -26,7 +26,8 @@
 	"tabs": {
 		"tools": "เครื่องมือ",
 		"resources": "ทรัพยากร",
-		"logs": "บันทึก"
+		"logs": "บันทึก",
+		"authDebug": "Auth Debug"
 	},
 	"emptyState": {
 		"noTools": "ไม่พบเครื่องมือ",
@@ -55,11 +56,27 @@
 	},
 	"serverStatus": {
 		"retrying": "กำลังลองใหม่...",
-		"retryConnection": "ลองเชื่อมต่อใหม่"
+		"retryConnection": "ลองเชื่อมต่อใหม่",
+		"signIn": "ลงชื่อเข้าใช้",
+		"signingIn": "กำลังลงชื่อเข้าใช้..."
 	},
 	"execution": {
 		"running": "กำลังทำงาน",
 		"completed": "เสร็จสมบูรณ์",
 		"error": "ข้อผิดพลาด"
+	},
+	"authDebug": {
+		"title": "ข้อมูล OAuth Debug",
+		"status": "สถานะ",
+		"tokenExpires": "Token หมดอายุ",
+		"scopes": "Scopes",
+		"refreshInfo": "ข้อมูล Refresh Token",
+		"hasRefreshToken": "มี Refresh Token",
+		"canRefresh": "รีเฟรชอัตโนมัติได้",
+		"tokenIssuedAt": "Token ออกเมื่อ",
+		"nextRefreshAt": "รีเฟรชถัดไปเมื่อ",
+		"lastRefreshAt": "รีเฟรชล่าสุดเมื่อ",
+		"tokenEndpoint": "Token Endpoint",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/tr/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Araçlar",
 		"resources": "Kaynaklar",
-		"logs": "Günlükler"
+		"logs": "Günlükler",
+		"authDebug": "Kimlik Doğrulama Hata Ayıklama"
 	},
 	"emptyState": {
 		"noTools": "Araç bulunamadı",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "Tekrar deneniyor...",
-		"retryConnection": "Bağlantıyı tekrar dene"
+		"retryConnection": "Bağlantıyı tekrar dene",
+		"signIn": "Oturum Aç",
+		"signingIn": "Oturum Açılıyor..."
 	},
 	"refreshMCP": "MCP Sunucularını Yenile",
 	"execution": {
 		"running": "Çalışıyor",
 		"completed": "Tamamlandı",
 		"error": "Hata"
+	},
+	"authDebug": {
+		"title": "OAuth Hata Ayıklama Bilgisi",
+		"status": "Durum",
+		"tokenExpires": "Token Sona Erme",
+		"scopes": "Kapsamlar",
+		"refreshInfo": "Yenileme Token Bilgisi",
+		"hasRefreshToken": "Yenileme Token'ı Var",
+		"canRefresh": "Otomatik Yenileyebilir",
+		"tokenIssuedAt": "Token Verilme Zamanı",
+		"nextRefreshAt": "Sonraki Yenileme Zamanı",
+		"lastRefreshAt": "Son Yenileme Zamanı",
+		"tokenEndpoint": "Token Uç Noktası",
+		"clientId": "İstemci Kimliği"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/uk/mcp.json

@@ -26,7 +26,8 @@
 	"tabs": {
 		"tools": "Інструменти",
 		"resources": "Ресурси",
-		"logs": "Логи"
+		"logs": "Логи",
+		"authDebug": "Налагодження автентифікації"
 	},
 	"emptyState": {
 		"noTools": "Інструменти не знайдено",
@@ -55,7 +56,23 @@
 	},
 	"serverStatus": {
 		"retrying": "Повторна спроба...",
-		"retryConnection": "Повторити підключення"
+		"retryConnection": "Повторити підключення",
+		"signIn": "Увійти",
+		"signingIn": "Вхід..."
+	},
+	"authDebug": {
+		"title": "Налагоджувальна інформація OAuth",
+		"status": "Статус",
+		"tokenExpires": "Термін дії токена",
+		"scopes": "Області доступу",
+		"refreshInfo": "Інформація про токен оновлення",
+		"hasRefreshToken": "Є токен оновлення",
+		"canRefresh": "Автоматичне оновлення",
+		"tokenIssuedAt": "Токен видано",
+		"nextRefreshAt": "Наступне оновлення",
+		"lastRefreshAt": "Останнє оновлення",
+		"tokenEndpoint": "Ендпоінт токена",
+		"clientId": "Client ID"
 	},
 	"execution": {
 		"running": "Виконується",

+ 19 - 2
webview-ui/src/i18n/locales/vi/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "Công cụ",
 		"resources": "Tài nguyên",
-		"logs": "Nhật ký"
+		"logs": "Nhật ký",
+		"authDebug": "Debug Xác thực"
 	},
 	"emptyState": {
 		"noTools": "Không tìm thấy công cụ",
@@ -54,7 +55,23 @@
 	},
 	"serverStatus": {
 		"retrying": "Đang thử lại...",
-		"retryConnection": "Thử kết nối lại"
+		"retryConnection": "Thử kết nối lại",
+		"signIn": "Đăng nhập",
+		"signingIn": "Đang đăng nhập..."
+	},
+	"authDebug": {
+		"title": "Thông tin Debug OAuth",
+		"status": "Trạng thái",
+		"tokenExpires": "Token hết hạn",
+		"scopes": "Phạm vi",
+		"refreshInfo": "Thông tin Refresh Token",
+		"hasRefreshToken": "Có Refresh Token",
+		"canRefresh": "Có thể tự động làm mới",
+		"tokenIssuedAt": "Token được cấp lúc",
+		"nextRefreshAt": "Lần làm mới tiếp theo",
+		"lastRefreshAt": "Lần làm mới cuối",
+		"tokenEndpoint": "Endpoint Token",
+		"clientId": "Client ID"
 	},
 	"refreshMCP": "Làm mới Máy chủ MCP",
 	"execution": {

+ 19 - 2
webview-ui/src/i18n/locales/zh-CN/mcp.json

@@ -25,7 +25,8 @@
 	"tabs": {
 		"tools": "工具",
 		"resources": "资源",
-		"logs": "日志"
+		"logs": "日志",
+		"authDebug": "认证调试"
 	},
 	"emptyState": {
 		"noTools": "未找到工具",
@@ -54,12 +55,28 @@
 	},
 	"serverStatus": {
 		"retrying": "重试中...",
-		"retryConnection": "重试连接"
+		"retryConnection": "重试连接",
+		"signIn": "登录",
+		"signingIn": "登录中..."
 	},
 	"refreshMCP": "刷新 MCP 服务器",
 	"execution": {
 		"running": "运行中",
 		"completed": "已完成",
 		"error": "错误"
+	},
+	"authDebug": {
+		"title": "OAuth 调试信息",
+		"status": "状态",
+		"tokenExpires": "Token 过期时间",
+		"scopes": "权限范围",
+		"refreshInfo": "刷新 Token 信息",
+		"hasRefreshToken": "有刷新 Token",
+		"canRefresh": "可自动刷新",
+		"tokenIssuedAt": "Token 签发时间",
+		"nextRefreshAt": "下次刷新时间",
+		"lastRefreshAt": "上次刷新时间",
+		"tokenEndpoint": "Token 端点",
+		"clientId": "Client ID"
 	}
 }

+ 19 - 2
webview-ui/src/i18n/locales/zh-TW/mcp.json

@@ -26,7 +26,8 @@
 	"tabs": {
 		"tools": "工具",
 		"resources": "資源",
-		"logs": "日誌"
+		"logs": "日誌",
+		"authDebug": "驗證偵錯"
 	},
 	"emptyState": {
 		"noTools": "找不到工具",
@@ -55,11 +56,27 @@
 	},
 	"serverStatus": {
 		"retrying": "重試中...",
-		"retryConnection": "重試連線"
+		"retryConnection": "重試連線",
+		"signIn": "登入",
+		"signingIn": "登入中..."
 	},
 	"execution": {
 		"running": "執行中",
 		"completed": "已完成",
 		"error": "錯誤"
+	},
+	"authDebug": {
+		"title": "OAuth 偵錯資訊",
+		"status": "狀態",
+		"tokenExpires": "Token 到期時間",
+		"scopes": "授權範圍",
+		"refreshInfo": "Refresh Token 資訊",
+		"hasRefreshToken": "具有 Refresh Token",
+		"canRefresh": "可自動更新",
+		"tokenIssuedAt": "Token 核發時間",
+		"nextRefreshAt": "下次更新時間",
+		"lastRefreshAt": "上次更新時間",
+		"tokenEndpoint": "Token 端點",
+		"clientId": "Client ID"
 	}
 }