소스 검색

Add McpHub and sync with McpView

Saoud Rizwan 1 년 전
부모
커밋
17d481d4d1

+ 105 - 1
package-lock.json

@@ -13,6 +13,7 @@
 				"@anthropic-ai/sdk": "^0.26.0",
 				"@anthropic-ai/sdk": "^0.26.0",
 				"@anthropic-ai/vertex-sdk": "^0.4.1",
 				"@anthropic-ai/vertex-sdk": "^0.4.1",
 				"@google/generative-ai": "^0.18.0",
 				"@google/generative-ai": "^0.18.0",
+				"@modelcontextprotocol/sdk": "^1.0.1",
 				"@types/clone-deep": "^4.0.4",
 				"@types/clone-deep": "^4.0.4",
 				"@types/pdf-parse": "^1.1.4",
 				"@types/pdf-parse": "^1.1.4",
 				"@types/turndown": "^5.0.5",
 				"@types/turndown": "^5.0.5",
@@ -38,7 +39,8 @@
 				"strip-ansi": "^7.1.0",
 				"strip-ansi": "^7.1.0",
 				"tree-sitter-wasms": "^0.1.11",
 				"tree-sitter-wasms": "^0.1.11",
 				"turndown": "^7.2.0",
 				"turndown": "^7.2.0",
-				"web-tree-sitter": "^0.22.6"
+				"web-tree-sitter": "^0.22.6",
+				"zod": "^3.23.8"
 			},
 			},
 			"devDependencies": {
 			"devDependencies": {
 				"@types/diff": "^5.2.1",
 				"@types/diff": "^5.2.1",
@@ -2777,6 +2779,17 @@
 			"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
 			"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
 			"license": "BSD-2-Clause"
 			"license": "BSD-2-Clause"
 		},
 		},
+		"node_modules/@modelcontextprotocol/sdk": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.0.1.tgz",
+			"integrity": "sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w==",
+			"license": "MIT",
+			"dependencies": {
+				"content-type": "^1.0.5",
+				"raw-body": "^3.0.0",
+				"zod": "^3.23.8"
+			}
+		},
 		"node_modules/@nodelib/fs.scandir": {
 		"node_modules/@nodelib/fs.scandir": {
 			"version": "2.1.5",
 			"version": "2.1.5",
 			"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 			"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -5322,6 +5335,15 @@
 			"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
 			"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
 			"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
 			"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
 		},
 		},
+		"node_modules/bytes": {
+			"version": "3.1.2",
+			"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+			"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/c8": {
 		"node_modules/c8": {
 			"version": "9.1.0",
 			"version": "9.1.0",
 			"resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz",
 			"resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz",
@@ -5667,6 +5689,15 @@
 			"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
 			"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
 			"license": "ISC"
 			"license": "ISC"
 		},
 		},
+		"node_modules/content-type": {
+			"version": "1.0.5",
+			"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+			"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
 		"node_modules/convert-source-map": {
 		"node_modules/convert-source-map": {
 			"version": "2.0.0",
 			"version": "2.0.0",
 			"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -5903,6 +5934,15 @@
 				"node": ">=0.4.0"
 				"node": ">=0.4.0"
 			}
 			}
 		},
 		},
+		"node_modules/depd": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+			"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/devtools-protocol": {
 		"node_modules/devtools-protocol": {
 			"version": "0.0.1342118",
 			"version": "0.0.1342118",
 			"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1342118.tgz",
 			"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1342118.tgz",
@@ -7366,6 +7406,22 @@
 				"entities": "^4.5.0"
 				"entities": "^4.5.0"
 			}
 			}
 		},
 		},
+		"node_modules/http-errors": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+			"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+			"license": "MIT",
+			"dependencies": {
+				"depd": "2.0.0",
+				"inherits": "2.0.4",
+				"setprototypeof": "1.2.0",
+				"statuses": "2.0.1",
+				"toidentifier": "1.0.1"
+			},
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/http-proxy-agent": {
 		"node_modules/http-proxy-agent": {
 			"version": "7.0.2",
 			"version": "7.0.2",
 			"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
 			"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -9614,6 +9670,21 @@
 				"safe-buffer": "^5.1.0"
 				"safe-buffer": "^5.1.0"
 			}
 			}
 		},
 		},
+		"node_modules/raw-body": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+			"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+			"license": "MIT",
+			"dependencies": {
+				"bytes": "3.1.2",
+				"http-errors": "2.0.0",
+				"iconv-lite": "0.6.3",
+				"unpipe": "1.0.0"
+			},
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/read-pkg": {
 		"node_modules/read-pkg": {
 			"version": "3.0.0",
 			"version": "3.0.0",
 			"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@@ -9990,6 +10061,12 @@
 			"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
 			"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
 			"license": "MIT"
 			"license": "MIT"
 		},
 		},
+		"node_modules/setprototypeof": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+			"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+			"license": "ISC"
+		},
 		"node_modules/shallow-clone": {
 		"node_modules/shallow-clone": {
 			"version": "3.0.1",
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
 			"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
@@ -10164,6 +10241,15 @@
 			"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
 			"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
 			"license": "BSD-3-Clause"
 			"license": "BSD-3-Clause"
 		},
 		},
+		"node_modules/statuses": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+			"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/stdin-discarder": {
 		"node_modules/stdin-discarder": {
 			"version": "0.1.0",
 			"version": "0.1.0",
 			"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
 			"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
@@ -10553,6 +10639,15 @@
 				"node": ">=8.0"
 				"node": ">=8.0"
 			}
 			}
 		},
 		},
+		"node_modules/toidentifier": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+			"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=0.6"
+			}
+		},
 		"node_modules/tr46": {
 		"node_modules/tr46": {
 			"version": "0.0.3",
 			"version": "0.0.3",
 			"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
 			"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -10805,6 +10900,15 @@
 				"node": ">= 4.0.0"
 				"node": ">= 4.0.0"
 			}
 			}
 		},
 		},
+		"node_modules/unpipe": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+			"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/uri-js": {
 		"node_modules/uri-js": {
 			"version": "4.4.1",
 			"version": "4.4.1",
 			"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
 			"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

+ 3 - 1
package.json

@@ -166,6 +166,7 @@
 		"@anthropic-ai/sdk": "^0.26.0",
 		"@anthropic-ai/sdk": "^0.26.0",
 		"@anthropic-ai/vertex-sdk": "^0.4.1",
 		"@anthropic-ai/vertex-sdk": "^0.4.1",
 		"@google/generative-ai": "^0.18.0",
 		"@google/generative-ai": "^0.18.0",
+		"@modelcontextprotocol/sdk": "^1.0.1",
 		"@types/clone-deep": "^4.0.4",
 		"@types/clone-deep": "^4.0.4",
 		"@types/pdf-parse": "^1.1.4",
 		"@types/pdf-parse": "^1.1.4",
 		"@types/turndown": "^5.0.5",
 		"@types/turndown": "^5.0.5",
@@ -191,6 +192,7 @@
 		"strip-ansi": "^7.1.0",
 		"strip-ansi": "^7.1.0",
 		"tree-sitter-wasms": "^0.1.11",
 		"tree-sitter-wasms": "^0.1.11",
 		"turndown": "^7.2.0",
 		"turndown": "^7.2.0",
-		"web-tree-sitter": "^0.22.6"
+		"web-tree-sitter": "^0.22.6",
+		"zod": "^3.23.8"
 	}
 	}
 }
 }

+ 6 - 0
src/core/Cline.ts

@@ -751,6 +751,12 @@ export class Cline {
 	}
 	}
 
 
 	async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
 	async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
+		// Wait for MCP servers to be connected before generating system prompt
+		await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
+			console.error("MCP servers failed to connect in time")
+		})
+		const mcpServers = this.providerRef.deref()?.mcpHub?.connections.map((conn) => conn.server)
+		console.log("mcpServers for system prompt:", JSON.stringify(mcpServers, null, 2))
 		let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false)
 		let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false)
 		if (this.customInstructions && this.customInstructions.trim()) {
 		if (this.customInstructions && this.customInstructions.trim()) {
 			// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
 			// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>

+ 22 - 1
src/core/webview/ClineProvider.ts

@@ -20,6 +20,7 @@ import { Cline } from "../Cline"
 import { openMention } from "../mentions"
 import { openMention } from "../mentions"
 import { getNonce } from "./getNonce"
 import { getNonce } from "./getNonce"
 import { getUri } from "./getUri"
 import { getUri } from "./getUri"
+import { McpHub } from "../../services/mcp/McpHub"
 
 
 /*
 /*
 https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
 https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -62,6 +63,7 @@ export const GlobalFileNames = {
 	apiConversationHistory: "api_conversation_history.json",
 	apiConversationHistory: "api_conversation_history.json",
 	uiMessages: "ui_messages.json",
 	uiMessages: "ui_messages.json",
 	openRouterModels: "openrouter_models.json",
 	openRouterModels: "openrouter_models.json",
+	mcpSettings: "cline_mcp_settings.json",
 }
 }
 
 
 export class ClineProvider implements vscode.WebviewViewProvider {
 export class ClineProvider implements vscode.WebviewViewProvider {
@@ -72,6 +74,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	private view?: vscode.WebviewView | vscode.WebviewPanel
 	private view?: vscode.WebviewView | vscode.WebviewPanel
 	private cline?: Cline
 	private cline?: Cline
 	private workspaceTracker?: WorkspaceTracker
 	private workspaceTracker?: WorkspaceTracker
+	mcpHub?: McpHub
 	private latestAnnouncementId = "oct-28-2024" // update to some unique identifier when we add a new announcement
 	private latestAnnouncementId = "oct-28-2024" // update to some unique identifier when we add a new announcement
 
 
 	constructor(
 	constructor(
@@ -81,6 +84,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		this.outputChannel.appendLine("ClineProvider instantiated")
 		this.outputChannel.appendLine("ClineProvider instantiated")
 		ClineProvider.activeInstances.add(this)
 		ClineProvider.activeInstances.add(this)
 		this.workspaceTracker = new WorkspaceTracker(this)
 		this.workspaceTracker = new WorkspaceTracker(this)
+		this.mcpHub = new McpHub(this)
 	}
 	}
 
 
 	/*
 	/*
@@ -104,6 +108,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		}
 		}
 		this.workspaceTracker?.dispose()
 		this.workspaceTracker?.dispose()
 		this.workspaceTracker = undefined
 		this.workspaceTracker = undefined
+		this.mcpHub?.dispose()
+		this.mcpHub = undefined
 		this.outputChannel.appendLine("Disposed all disposables")
 		this.outputChannel.appendLine("Disposed all disposables")
 		ClineProvider.activeInstances.delete(this)
 		ClineProvider.activeInstances.delete(this)
 	}
 	}
@@ -485,6 +491,21 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						}
 						}
 
 
 						break
 						break
+					case "openMcpSettings": {
+						const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
+						if (mcpSettingsFilePath) {
+							openFile(mcpSettingsFilePath)
+						}
+						break
+					}
+					case "retryMcpServer": {
+						try {
+							await this.mcpHub?.retryConnection(message.text!)
+						} catch (error) {
+							console.error(`Failed to retry connection for ${message.text}:`, error)
+						}
+						break
+					}
 					// Add more switch case statements here as more webview message commands
 					// Add more switch case statements here as more webview message commands
 					// are created within the webview context (i.e. inside media/main.js)
 					// are created within the webview context (i.e. inside media/main.js)
 				}
 				}
@@ -567,7 +588,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
 		// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
 	}
 	}
 
 
-	private async ensureCacheDirectoryExists(): Promise<string> {
+	async ensureCacheDirectoryExists(): Promise<string> {
 		const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
 		const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
 		await fs.mkdir(cacheDir, { recursive: true })
 		await fs.mkdir(cacheDir, { recursive: true })
 		return cacheDir
 		return cacheDir

+ 314 - 0
src/services/mcp/McpHub.ts

@@ -0,0 +1,314 @@
+import { Client } from "@modelcontextprotocol/sdk/client/index.js"
+import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
+import { ListResourcesResultSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js"
+import deepEqual from "fast-deep-equal"
+import * as fs from "fs/promises"
+import * as path from "path"
+import * as vscode from "vscode"
+import { z } from "zod"
+import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
+import { McpResource, McpServer, McpTool } from "../../shared/mcp"
+import { fileExistsAtPath } from "../../utils/fs"
+import { arePathsEqual } from "../../utils/path"
+
+export type McpConnection = {
+	server: McpServer
+	client: Client
+	transport: StdioClientTransport
+}
+
+// StdioServerParameters
+const StdioConfigSchema = z.object({
+	command: z.string(),
+	args: z.array(z.string()).optional(),
+	env: z.record(z.string()).optional(),
+})
+
+const McpSettingsSchema = z.object({
+	mcpServers: z.record(StdioConfigSchema),
+})
+
+export class McpHub {
+	private providerRef: WeakRef<ClineProvider>
+	private settingsWatcher?: vscode.FileSystemWatcher
+	private disposables: vscode.Disposable[] = []
+	connections: McpConnection[] = []
+	isConnecting: boolean = false
+
+	constructor(provider: ClineProvider) {
+		this.providerRef = new WeakRef(provider)
+		this.watchMcpSettingsFile()
+		this.initializeMcpServers()
+	}
+
+	async getMcpSettingsFilePath(): Promise<string> {
+		const provider = this.providerRef.deref()
+		if (!provider) {
+			throw new Error("Provider not available")
+		}
+		const mcpSettingsFilePath = path.join(await provider.ensureCacheDirectoryExists(), GlobalFileNames.mcpSettings)
+		const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
+		if (!fileExists) {
+			await fs.writeFile(
+				mcpSettingsFilePath,
+				`{
+  "mcpServers": {
+    
+  }
+}`,
+			)
+		}
+		return mcpSettingsFilePath
+	}
+
+	private async watchMcpSettingsFile(): Promise<void> {
+		const settingsPath = await this.getMcpSettingsFilePath()
+		this.disposables.push(
+			vscode.workspace.onDidSaveTextDocument(async (document) => {
+				if (arePathsEqual(document.uri.fsPath, settingsPath)) {
+					const content = await fs.readFile(settingsPath, "utf-8")
+					const errorMessage =
+						"Invalid MCP settings format. Please ensure your settings follow the correct JSON format."
+					let config: any
+					try {
+						config = JSON.parse(content)
+					} catch (error) {
+						vscode.window.showErrorMessage(errorMessage)
+						return
+					}
+					const result = McpSettingsSchema.safeParse(config)
+					if (!result.success) {
+						vscode.window.showErrorMessage(errorMessage)
+						return
+					}
+					try {
+						vscode.window.showInformationMessage("Updating MCP servers...")
+						await this.updateServerConnections(result.data.mcpServers || {})
+						vscode.window.showInformationMessage("MCP servers updated")
+					} catch (error) {
+						console.error("Failed to process MCP settings change:", error)
+					}
+				}
+			}),
+		)
+	}
+
+	private async initializeMcpServers(): Promise<void> {
+		try {
+			const settingsPath = await this.getMcpSettingsFilePath()
+			const content = await fs.readFile(settingsPath, "utf-8")
+			const config = JSON.parse(content)
+			await this.updateServerConnections(config.mcpServers || {})
+		} catch (error) {
+			console.error("Failed to initialize MCP servers:", error)
+		}
+	}
+
+	private async connectToServer(name: string, config: StdioServerParameters): Promise<void> {
+		// Remove existing connection if it exists
+		this.connections = this.connections.filter((conn) => conn.server.name !== name)
+
+		try {
+			// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
+			const client = new Client(
+				{
+					name: "Cline",
+					version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
+				},
+				{
+					capabilities: {},
+				},
+			)
+
+			const transport = new StdioClientTransport({
+				command: config.command,
+				args: config.args,
+				env: {
+					...config.env,
+					...(process.env.PATH ? { PATH: process.env.PATH } : {}),
+					// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
+				},
+			})
+
+			transport.onerror = (error) => {
+				console.error(`Transport error for "${name}":`, error)
+				const connection = this.connections.find((conn) => conn.server.name === name)
+				if (connection) {
+					connection.server.status = "disconnected"
+					connection.server.error = error.message
+				}
+			}
+
+			transport.onclose = () => {
+				const connection = this.connections.find((conn) => conn.server.name === name)
+				if (connection) {
+					connection.server.status = "disconnected"
+				}
+			}
+
+			// If the config is invalid, show an error
+			if (!StdioConfigSchema.safeParse(config).success) {
+				console.error(`Invalid config for "${name}": missing or invalid parameters`)
+				const connection: McpConnection = {
+					server: {
+						name,
+						config: JSON.stringify(config),
+						status: "disconnected",
+						error: "Invalid config: missing or invalid parameters",
+					},
+					client,
+					transport,
+				}
+				this.connections.push(connection)
+				await this.notifyWebviewOfServerChanges()
+				return
+			}
+
+			await client.connect(transport)
+			const connection: McpConnection = {
+				server: {
+					name,
+					config: JSON.stringify(config),
+					status: "connecting",
+				},
+				client,
+				transport,
+			}
+			this.connections.push(connection)
+			connection.server.status = "connected"
+
+			// After successful connection, fetch tools and resources
+			connection.server.tools = await this.fetchTools(name)
+			connection.server.resources = await this.fetchResources(name)
+
+			await this.notifyWebviewOfServerChanges()
+		} catch (error) {
+			// Update status with error
+			const connection = this.connections.find((conn) => conn.server.name === name)
+			if (connection) {
+				connection.server.status = "disconnected"
+				connection.server.error = error instanceof Error ? error.message : String(error)
+			}
+			await this.notifyWebviewOfServerChanges()
+			throw error
+		}
+	}
+
+	private async fetchTools(serverName: string): Promise<McpTool[]> {
+		try {
+			const response = await this.connections
+				.find((conn) => conn.server.name === serverName)
+				?.client.request({ method: "tools/list" }, ListToolsResultSchema)
+			return response?.tools || []
+		} catch (error) {
+			console.error(`Failed to fetch tools for ${serverName}:`, error)
+			return []
+		}
+	}
+
+	private async fetchResources(serverName: string): Promise<McpResource[]> {
+		try {
+			const response = await this.connections
+				.find((conn) => conn.server.name === serverName)
+				?.client.request({ method: "resources/list" }, ListResourcesResultSchema)
+			return response?.resources || []
+		} catch (error) {
+			console.error(`Failed to fetch resources for ${serverName}:`, error)
+			return []
+		}
+	}
+
+	async deleteConnection(name: string): Promise<void> {
+		const connection = this.connections.find((conn) => conn.server.name === name)
+		if (connection) {
+			try {
+				await connection.transport.close()
+				await connection.client.close()
+			} catch (error) {
+				console.error(`Failed to close transport for ${name}:`, error)
+			}
+			this.connections = this.connections.filter((conn) => conn.server.name !== name)
+			await this.notifyWebviewOfServerChanges()
+		}
+	}
+
+	async updateServerConnections(newServers: Record<string, any>): Promise<void> {
+		this.isConnecting = true
+		const currentNames = new Set(this.connections.map((conn) => conn.server.name))
+		const newNames = new Set(Object.keys(newServers))
+
+		// Delete removed servers
+		for (const name of currentNames) {
+			if (!newNames.has(name)) {
+				await this.deleteConnection(name)
+				console.log(`Deleted MCP server: ${name}`)
+			}
+		}
+
+		// Update or add servers
+		for (const [name, config] of Object.entries(newServers)) {
+			const currentConnection = this.connections.find((conn) => conn.server.name === name)
+
+			if (!currentConnection) {
+				// New server - connect
+				try {
+					await this.connectToServer(name, config)
+				} catch (error) {
+					console.error(`Failed to connect to new MCP server ${name}:`, error)
+				}
+			} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
+				// Existing server with changed config - reconnect
+				try {
+					await this.deleteConnection(name)
+					await this.connectToServer(name, config)
+					console.log(`Reconnected MCP server with updated config: ${name}`)
+				} catch (error) {
+					console.error(`Failed to reconnect MCP server ${name}:`, error)
+				}
+			}
+			// If server exists with same config, do nothing
+		}
+		this.isConnecting = false
+	}
+
+	async retryConnection(serverName: string): Promise<void> {
+		this.isConnecting = true
+		const provider = this.providerRef.deref()
+		if (!provider) {
+			return
+		}
+
+		// Get existing connection and update its status
+		const connection = this.connections.find((conn) => conn.server.name === serverName)
+		const config = connection?.server.config
+		if (config) {
+			// Try to connect again using existing config
+			await this.connectToServer(serverName, JSON.parse(config))
+		}
+
+		await this.notifyWebviewOfServerChanges()
+		this.isConnecting = false
+	}
+
+	private async notifyWebviewOfServerChanges(): Promise<void> {
+		await this.providerRef.deref()?.postMessageToWebview({
+			type: "mcpServers",
+			mcpServers: this.connections.map((connection) => connection.server),
+		})
+	}
+
+	async dispose(): Promise<void> {
+		for (const connection of this.connections) {
+			try {
+				await this.deleteConnection(connection.server.name)
+			} catch (error) {
+				console.error(`Failed to close connection for ${connection.server.name}:`, error)
+			}
+		}
+		this.connections = []
+		if (this.settingsWatcher) {
+			this.settingsWatcher.dispose()
+		}
+		this.disposables.forEach((d) => d.dispose())
+	}
+}

+ 3 - 0
src/shared/ExtensionMessage.ts

@@ -2,6 +2,7 @@
 
 
 import { ApiConfiguration, ModelInfo } from "./api"
 import { ApiConfiguration, ModelInfo } from "./api"
 import { HistoryItem } from "./HistoryItem"
 import { HistoryItem } from "./HistoryItem"
+import { McpServer } from "./mcp"
 
 
 // webview will hold state
 // webview will hold state
 export interface ExtensionMessage {
 export interface ExtensionMessage {
@@ -16,6 +17,7 @@ export interface ExtensionMessage {
 		| "invoke"
 		| "invoke"
 		| "partialMessage"
 		| "partialMessage"
 		| "openRouterModels"
 		| "openRouterModels"
+		| "mcpServers"
 	text?: string
 	text?: string
 	action?:
 	action?:
 		| "chatButtonClicked"
 		| "chatButtonClicked"
@@ -31,6 +33,7 @@ export interface ExtensionMessage {
 	filePaths?: string[]
 	filePaths?: string[]
 	partialMessage?: ClineMessage
 	partialMessage?: ClineMessage
 	openRouterModels?: Record<string, ModelInfo>
 	openRouterModels?: Record<string, ModelInfo>
+	mcpServers?: McpServer[]
 }
 }
 
 
 export interface ExtensionState {
 export interface ExtensionState {

+ 2 - 0
src/shared/WebviewMessage.ts

@@ -23,6 +23,8 @@ export interface WebviewMessage {
 		| "openMention"
 		| "openMention"
 		| "cancelTask"
 		| "cancelTask"
 		| "refreshOpenRouterModels"
 		| "refreshOpenRouterModels"
+		| "openMcpSettings"
+		| "retryMcpServer"
 	text?: string
 	text?: string
 	askResponse?: ClineAskResponse
 	askResponse?: ClineAskResponse
 	apiConfiguration?: ApiConfiguration
 	apiConfiguration?: ApiConfiguration

+ 21 - 0
src/shared/mcp.ts

@@ -0,0 +1,21 @@
+export type McpServer = {
+	name: string
+	config: string
+	status: "connected" | "connecting" | "disconnected"
+	error?: string
+	tools?: McpTool[]
+	resources?: McpResource[]
+}
+
+export type McpTool = {
+	name: string
+	description?: string
+	inputSchema?: object
+}
+
+export type McpResource = {
+	uri: string
+	name: string
+	mimeType?: string
+	description?: string
+}

+ 92 - 211
webview-ui/src/components/mcp/McpView.tsx

@@ -1,119 +1,75 @@
-import {
-	VSCodeButton,
-	VSCodeDivider,
-	VSCodeTextArea,
-	VSCodeTextField,
-	VSCodeTag,
-	VSCodePanelTab,
-	VSCodePanelView,
-	VSCodeDataGrid,
-	VSCodeDataGridRow,
-	VSCodeDataGridCell,
-	VSCodePanels,
-} from "@vscode/webview-ui-toolkit/react"
+import { VSCodeButton, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
 import { useState } from "react"
 import { useState } from "react"
-
-type McpServer = {
-	name: string
-	config: string // JSON config
-	status: "connected" | "connecting" | "disconnected"
-	error?: string
-	tools?: any[] // We'll type this properly later
-	resources?: any[] // We'll type this properly later
-}
+import { vscode } from "../../utils/vscode"
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import { McpServer } from "../../../../src/shared/mcp"
 
 
 type McpViewProps = {
 type McpViewProps = {
 	onDone: () => void
 	onDone: () => void
 }
 }
 
 
 const McpView = ({ onDone }: McpViewProps) => {
 const McpView = ({ onDone }: McpViewProps) => {
-	const [isAdding, setIsAdding] = useState(false)
-	const [servers, setServers] = useState<McpServer[]>([
-		// Add some mock servers for testing
-		{
-			name: "local-tools",
-			config: JSON.stringify({
-				mcpServers: {
-					"local-tools": {
-						command: "npx",
-						args: ["-y", "@modelcontextprotocol/server-tools"],
-					},
-				},
-			}),
-			status: "connected",
-			tools: [
-				{
-					name: "execute_command",
-					description: "Run a shell command on the local system",
-				},
-				{
-					name: "read_file",
-					description: "Read contents of a file from the filesystem",
-				},
-			],
-		},
-		{
-			name: "postgres-db",
-			config: JSON.stringify({
-				mcpServers: {
-					"postgres-db": {
-						command: "npx",
-						args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
-					},
-				},
-			}),
-			status: "disconnected",
-			error: "Failed to connect to database: Connection refused",
-		},
-		{
-			name: "github-tools",
-			config: JSON.stringify({
-				mcpServers: {
-					"github-tools": {
-						command: "npx",
-						args: ["-y", "@modelcontextprotocol/server-github"],
-					},
-				},
-			}),
-			status: "connecting",
-			resources: [
-				{
-					uri: "github://repo/issues",
-					name: "Repository Issues",
-				},
-				{
-					uri: "github://repo/pulls",
-					name: "Pull Requests",
-				},
-			],
-		},
-	])
-	const [configInput, setConfigInput] = useState("")
-
-	const handleAddServer = () => {
-		try {
-			const config = JSON.parse(configInput)
-			const serverName = Object.keys(config.mcpServers)[0]
-
-			setServers((prev) => [
-				...prev,
-				{
-					name: serverName,
-					config: configInput,
-					status: "connecting",
-				},
-			])
-
-			setIsAdding(false)
-			setConfigInput("")
-
-			// Here you would trigger the actual server connection
-			// and update its status/tools/resources accordingly
-		} catch (e) {
-			// Handle invalid JSON
-			console.error("Invalid server configuration:", e)
-		}
-	}
+	const { mcpServers: servers } = useExtensionState()
+	// const [servers, setServers] = useState<McpServer[]>([
+	// 	// Add some mock servers for testing
+	// 	{
+	// 		name: "local-tools",
+	// 		config: JSON.stringify({
+	// 			mcpServers: {
+	// 				"local-tools": {
+	// 					command: "npx",
+	// 					args: ["-y", "@modelcontextprotocol/server-tools"],
+	// 				},
+	// 			},
+	// 		}),
+	// 		status: "connected",
+	// 		tools: [
+	// 			{
+	// 				name: "execute_command",
+	// 				description: "Run a shell command on the local system",
+	// 			},
+	// 			{
+	// 				name: "read_file",
+	// 				description: "Read contents of a file from the filesystem",
+	// 			},
+	// 		],
+	// 	},
+	// 	{
+	// 		name: "postgres-db",
+	// 		config: JSON.stringify({
+	// 			mcpServers: {
+	// 				"postgres-db": {
+	// 					command: "npx",
+	// 					args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
+	// 				},
+	// 			},
+	// 		}),
+	// 		status: "disconnected",
+	// 		error: "Failed to connect to database: Connection refused",
+	// 	},
+	// 	{
+	// 		name: "github-tools",
+	// 		config: JSON.stringify({
+	// 			mcpServers: {
+	// 				"github-tools": {
+	// 					command: "npx",
+	// 					args: ["-y", "@modelcontextprotocol/server-github"],
+	// 				},
+	// 			},
+	// 		}),
+	// 		status: "connecting",
+	// 		resources: [
+	// 			{
+	// 				uri: "github://repo/issues",
+	// 				name: "Repository Issues",
+	// 			},
+	// 			{
+	// 				uri: "github://repo/pulls",
+	// 				name: "Pull Requests",
+	// 			},
+	// 		],
+	// 	},
+	// ])
 
 
 	return (
 	return (
 		<div
 		<div
@@ -126,7 +82,6 @@ const McpView = ({ onDone }: McpViewProps) => {
 				display: "flex",
 				display: "flex",
 				flexDirection: "column",
 				flexDirection: "column",
 			}}>
 			}}>
-			{/* Fixed Header */}
 			<div
 			<div
 				style={{
 				style={{
 					display: "flex",
 					display: "flex",
@@ -138,11 +93,10 @@ const McpView = ({ onDone }: McpViewProps) => {
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
 			</div>
 			</div>
 
 
-			{/* Scrollable Content */}
 			<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
 			<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
 				<p style={{ color: "var(--vscode-foreground)", fontSize: "13px" }}>
 				<p style={{ color: "var(--vscode-foreground)", fontSize: "13px" }}>
 					MCP (Model Context Protocol) enables AI models to access external tools and data through
 					MCP (Model Context Protocol) enables AI models to access external tools and data through
-					standardized interfaces. Add MCP servers to extend Claude's capabilities with custom functionality
+					standardized interfaces. These MCP servers extend Claude's capabilities with custom functionality
 					and real-time data access.
 					and real-time data access.
 				</p>
 				</p>
 
 
@@ -151,51 +105,22 @@ const McpView = ({ onDone }: McpViewProps) => {
 					{servers.map((server) => (
 					{servers.map((server) => (
 						<ServerRow key={server.name} server={server} />
 						<ServerRow key={server.name} server={server} />
 					))}
 					))}
+				</div>
 
 
-					{/* Add Server UI as a row */}
-					{isAdding ? (
-						<div
-							style={{
-								padding: "12px",
-								background: "var(--vscode-list-hoverBackground)",
-								borderRadius: "4px",
-								display: "flex",
-								flexDirection: "column",
-								gap: "10px",
-							}}>
-							<b style={{ color: "var(--vscode-foreground)" }}>New MCP Server</b>
-							<p style={{ color: "var(--vscode-descriptionForeground)", fontSize: "13px", margin: "0" }}>
-								Enter the MCP server configuration in JSON format. You can find this configuration in
-								the setup instructions for your MCP server. The config defines how to start and connect
-								to the server, typically specifying a command and arguments.
-							</p>
-							<VSCodeTextArea
-								rows={4}
-								placeholder='{"mcpServers": {"server-name": {"command": "...", "args": [...]}}}'
-								value={configInput}
-								onChange={(e) => setConfigInput((e.target as HTMLTextAreaElement).value)}
-							/>
-							<div style={{ display: "flex", gap: "10px" }}>
-								<VSCodeButton style={{ flex: 1 }} onClick={handleAddServer}>
-									Save
-								</VSCodeButton>
-								<VSCodeButton
-									style={{ flex: 1 }}
-									appearance="secondary"
-									onClick={() => setIsAdding(false)}>
-									Cancel
-								</VSCodeButton>
-							</div>
-						</div>
-					) : (
-						<VSCodeButton style={{ width: "100%" }} onClick={() => setIsAdding(true)}>
-							<span className="codicon codicon-add" style={{ marginRight: "6px" }}></span>
-							Add MCP Server
-						</VSCodeButton>
-					)}
+				{/* Edit Settings Button */}
+				<div style={{ marginTop: "10px", width: "100%" }}>
+					<VSCodeButton
+						appearance="secondary"
+						style={{ width: "100%" }}
+						onClick={() => {
+							vscode.postMessage({ type: "openMcpSettings" })
+						}}>
+						<span className="codicon codicon-edit" style={{ marginRight: "6px" }}></span>
+						Edit MCP Settings
+					</VSCodeButton>
 				</div>
 				</div>
 
 
-				{/* Add bottom padding for scrolling */}
+				{/* Bottom padding */}
 				<div style={{ height: "20px" }} />
 				<div style={{ height: "20px" }} />
 			</div>
 			</div>
 		</div>
 		</div>
@@ -205,8 +130,6 @@ const McpView = ({ onDone }: McpViewProps) => {
 // Server Row Component
 // Server Row Component
 const ServerRow = ({ server }: { server: McpServer }) => {
 const ServerRow = ({ server }: { server: McpServer }) => {
 	const [isExpanded, setIsExpanded] = useState(false)
 	const [isExpanded, setIsExpanded] = useState(false)
-	const [isEditing, setIsEditing] = useState(false)
-	const [editConfig, setEditConfig] = useState(server.config)
 
 
 	const getStatusColor = () => {
 	const getStatusColor = () => {
 		switch (server.status) {
 		switch (server.status) {
@@ -219,23 +142,19 @@ const ServerRow = ({ server }: { server: McpServer }) => {
 		}
 		}
 	}
 	}
 
 
-	const handleSaveConfig = () => {
-		try {
-			JSON.parse(editConfig) // Validate JSON
-			// Here you would update the server config
-			setIsEditing(false)
-		} catch (e) {
-			console.error("Invalid JSON config:", e)
-		}
-	}
-
-	// Don't allow expansion if server has error
 	const handleRowClick = () => {
 	const handleRowClick = () => {
 		if (!server.error) {
 		if (!server.error) {
 			setIsExpanded(!isExpanded)
 			setIsExpanded(!isExpanded)
 		}
 		}
 	}
 	}
 
 
+	const handleRetry = () => {
+		vscode.postMessage({
+			type: "retryMcpServer",
+			text: server.name,
+		})
+	}
+
 	return (
 	return (
 		<div style={{ marginBottom: "10px" }}>
 		<div style={{ marginBottom: "10px" }}>
 			<div
 			<div
@@ -271,18 +190,21 @@ const ServerRow = ({ server }: { server: McpServer }) => {
 					style={{
 					style={{
 						padding: "8px",
 						padding: "8px",
 						fontSize: "13px",
 						fontSize: "13px",
-						color: "var(--vscode-testing-iconFailed)",
 						background: "var(--vscode-list-hoverBackground)",
 						background: "var(--vscode-list-hoverBackground)",
 						borderRadius: "0 0 4px 4px",
 						borderRadius: "0 0 4px 4px",
 					}}>
 					}}>
-					{server.error}
+					<div style={{ color: "var(--vscode-testing-iconFailed)", marginBottom: "8px" }}>{server.error}</div>
+					<VSCodeButton appearance="secondary" onClick={handleRetry}>
+						<span className="codicon codicon-debug-restart" style={{ marginRight: "6px" }}></span>
+						Retry Connection
+					</VSCodeButton>
 				</div>
 				</div>
 			) : (
 			) : (
 				isExpanded && (
 				isExpanded && (
 					<div
 					<div
 						style={{
 						style={{
 							background: "var(--vscode-list-hoverBackground)",
 							background: "var(--vscode-list-hoverBackground)",
-							padding: "0 12px 12px 12px",
+							padding: "0 12px 0 12px",
 							fontSize: "13px",
 							fontSize: "13px",
 							borderRadius: "0 0 4px 4px",
 							borderRadius: "0 0 4px 4px",
 						}}>
 						}}>
@@ -361,47 +283,6 @@ const ServerRow = ({ server }: { server: McpServer }) => {
 								)}
 								)}
 							</VSCodePanelView>
 							</VSCodePanelView>
 						</VSCodePanels>
 						</VSCodePanels>
-
-						{/* Edit/Remove Buttons */}
-						<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginTop: "0px" }}>
-							{isEditing ? (
-								<>
-									<VSCodeTextArea
-										rows={4}
-										value={editConfig}
-										onChange={(e) => setEditConfig((e.target as HTMLTextAreaElement).value)}
-										style={{ width: "100%" }}
-									/>
-									<div style={{ display: "flex", gap: "8px" }}>
-										<VSCodeButton onClick={handleSaveConfig} style={{ flex: 1 }}>
-											Save
-										</VSCodeButton>
-										<VSCodeButton
-											appearance="secondary"
-											onClick={() => setIsEditing(false)}
-											style={{ flex: 1 }}>
-											Cancel
-										</VSCodeButton>
-									</div>
-								</>
-							) : (
-								<div style={{ display: "flex", gap: "8px" }}>
-									<VSCodeButton
-										appearance="secondary"
-										onClick={() => setIsEditing(true)}
-										style={{ flex: 1 }}>
-										Edit
-									</VSCodeButton>
-									<VSCodeButton
-										appearance="secondary"
-										style={{
-											flex: 1,
-										}}>
-										Remove
-									</VSCodeButton>
-								</div>
-							)}
-						</div>
 					</div>
 					</div>
 				)
 				)
 			)}
 			)}

+ 8 - 0
webview-ui/src/context/ExtensionStateContext.tsx

@@ -10,12 +10,14 @@ import {
 import { vscode } from "../utils/vscode"
 import { vscode } from "../utils/vscode"
 import { convertTextMateToHljs } from "../utils/textMateToHljs"
 import { convertTextMateToHljs } from "../utils/textMateToHljs"
 import { findLastIndex } from "../../../src/shared/array"
 import { findLastIndex } from "../../../src/shared/array"
+import { McpServer } from "../../../src/shared/mcp"
 
 
 interface ExtensionStateContextType extends ExtensionState {
 interface ExtensionStateContextType extends ExtensionState {
 	didHydrateState: boolean
 	didHydrateState: boolean
 	showWelcome: boolean
 	showWelcome: boolean
 	theme: any
 	theme: any
 	openRouterModels: Record<string, ModelInfo>
 	openRouterModels: Record<string, ModelInfo>
+	mcpServers: McpServer[]
 	filePaths: string[]
 	filePaths: string[]
 	setApiConfiguration: (config: ApiConfiguration) => void
 	setApiConfiguration: (config: ApiConfiguration) => void
 	setCustomInstructions: (value?: string) => void
 	setCustomInstructions: (value?: string) => void
@@ -39,6 +41,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 	const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
 	const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
 		[openRouterDefaultModelId]: openRouterDefaultModelInfo,
 		[openRouterDefaultModelId]: openRouterDefaultModelInfo,
 	})
 	})
+	const [mcpServers, setMcpServers] = useState<McpServer[]>([])
 
 
 	const handleMessage = useCallback((event: MessageEvent) => {
 	const handleMessage = useCallback((event: MessageEvent) => {
 		const message: ExtensionMessage = event.data
 		const message: ExtensionMessage = event.data
@@ -95,6 +98,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 				})
 				})
 				break
 				break
 			}
 			}
+			case "mcpServers": {
+				setMcpServers(message.mcpServers ?? [])
+				break
+			}
 		}
 		}
 	}, [])
 	}, [])
 
 
@@ -110,6 +117,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		showWelcome,
 		showWelcome,
 		theme,
 		theme,
 		openRouterModels,
 		openRouterModels,
+		mcpServers,
 		filePaths,
 		filePaths,
 		setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
 		setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
 		setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
 		setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),