mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 04:41:16 -05:00
Add McpHub and sync with McpView
This commit is contained in:
106
package-lock.json
generated
106
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
src/services/mcp/McpHub.ts
Normal file
314
src/services/mcp/McpHub.ts
Normal file
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
src/shared/mcp.ts
Normal file
21
src/shared/mcp.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,119 +1,75 @@
|
|||||||
import {
|
import { VSCodeButton, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
|
||||||
VSCodeButton,
|
|
||||||
VSCodeDivider,
|
|
||||||
VSCodeTextArea,
|
|
||||||
VSCodeTextField,
|
|
||||||
VSCodeTag,
|
|
||||||
VSCodePanelTab,
|
|
||||||
VSCodePanelView,
|
|
||||||
VSCodeDataGrid,
|
|
||||||
VSCodeDataGridRow,
|
|
||||||
VSCodeDataGridCell,
|
|
||||||
VSCodePanels,
|
|
||||||
} from "@vscode/webview-ui-toolkit/react"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import { vscode } from "../../utils/vscode"
|
||||||
type McpServer = {
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
name: string
|
import { McpServer } from "../../../../src/shared/mcp"
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type McpViewProps = {
|
type McpViewProps = {
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const McpView = ({ onDone }: McpViewProps) => {
|
const McpView = ({ onDone }: McpViewProps) => {
|
||||||
const [isAdding, setIsAdding] = useState(false)
|
const { mcpServers: servers } = useExtensionState()
|
||||||
const [servers, setServers] = useState<McpServer[]>([
|
// const [servers, setServers] = useState<McpServer[]>([
|
||||||
// Add some mock servers for testing
|
// // Add some mock servers for testing
|
||||||
{
|
// {
|
||||||
name: "local-tools",
|
// name: "local-tools",
|
||||||
config: JSON.stringify({
|
// config: JSON.stringify({
|
||||||
mcpServers: {
|
// mcpServers: {
|
||||||
"local-tools": {
|
// "local-tools": {
|
||||||
command: "npx",
|
// command: "npx",
|
||||||
args: ["-y", "@modelcontextprotocol/server-tools"],
|
// args: ["-y", "@modelcontextprotocol/server-tools"],
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
}),
|
// }),
|
||||||
status: "connected",
|
// status: "connected",
|
||||||
tools: [
|
// tools: [
|
||||||
{
|
// {
|
||||||
name: "execute_command",
|
// name: "execute_command",
|
||||||
description: "Run a shell command on the local system",
|
// description: "Run a shell command on the local system",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "read_file",
|
// name: "read_file",
|
||||||
description: "Read contents of a file from the filesystem",
|
// description: "Read contents of a file from the filesystem",
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "postgres-db",
|
// name: "postgres-db",
|
||||||
config: JSON.stringify({
|
// config: JSON.stringify({
|
||||||
mcpServers: {
|
// mcpServers: {
|
||||||
"postgres-db": {
|
// "postgres-db": {
|
||||||
command: "npx",
|
// command: "npx",
|
||||||
args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
|
// args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
}),
|
// }),
|
||||||
status: "disconnected",
|
// status: "disconnected",
|
||||||
error: "Failed to connect to database: Connection refused",
|
// error: "Failed to connect to database: Connection refused",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "github-tools",
|
// name: "github-tools",
|
||||||
config: JSON.stringify({
|
// config: JSON.stringify({
|
||||||
mcpServers: {
|
// mcpServers: {
|
||||||
"github-tools": {
|
// "github-tools": {
|
||||||
command: "npx",
|
// command: "npx",
|
||||||
args: ["-y", "@modelcontextprotocol/server-github"],
|
// args: ["-y", "@modelcontextprotocol/server-github"],
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
}),
|
// }),
|
||||||
status: "connecting",
|
// status: "connecting",
|
||||||
resources: [
|
// resources: [
|
||||||
{
|
// {
|
||||||
uri: "github://repo/issues",
|
// uri: "github://repo/issues",
|
||||||
name: "Repository Issues",
|
// name: "Repository Issues",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
uri: "github://repo/pulls",
|
// uri: "github://repo/pulls",
|
||||||
name: "Pull Requests",
|
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 */}
|
{/* Edit Settings Button */}
|
||||||
{isAdding ? (
|
<div style={{ marginTop: "10px", width: "100%" }}>
|
||||||
<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
|
<VSCodeButton
|
||||||
style={{ flex: 1 }}
|
|
||||||
appearance="secondary"
|
appearance="secondary"
|
||||||
onClick={() => setIsAdding(false)}>
|
style={{ width: "100%" }}
|
||||||
Cancel
|
onClick={() => {
|
||||||
|
vscode.postMessage({ type: "openMcpSettings" })
|
||||||
|
}}>
|
||||||
|
<span className="codicon codicon-edit" style={{ marginRight: "6px" }}></span>
|
||||||
|
Edit MCP Settings
|
||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<VSCodeButton style={{ width: "100%" }} onClick={() => setIsAdding(true)}>
|
|
||||||
<span className="codicon codicon-add" style={{ marginRight: "6px" }}></span>
|
|
||||||
Add MCP Server
|
|
||||||
</VSCodeButton>
|
|
||||||
)}
|
|
||||||
</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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 })),
|
||||||
|
|||||||
Reference in New Issue
Block a user