mirror of
https://github.com/cline/cline.git
synced 2026-09-08 22:13:11 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2fa53cd89 | ||
|
|
fd01132786 | ||
|
|
bcc0a0e6ee | ||
|
|
6424f1251b | ||
|
|
f55d794a1a |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate restartMcpServer to protobus
|
||||
@@ -11,6 +11,7 @@ service McpService {
|
||||
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
|
||||
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
|
||||
rpc downloadMcp(StringRequest) returns (Empty);
|
||||
rpc restartMcpServer(StringRequest) returns (McpServers);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
|
||||
@@ -499,14 +499,6 @@ export class Controller {
|
||||
this.refreshTotalTasksSize()
|
||||
break
|
||||
}
|
||||
case "restartMcpServer": {
|
||||
try {
|
||||
await this.mcpHub?.restartConnection(message.text!)
|
||||
} catch (error) {
|
||||
console.error(`Failed to retry connection for ${message.text}:`, error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "deleteMcpServer": {
|
||||
if (message.serverName) {
|
||||
this.mcpHub?.deleteServer(message.serverName)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { registerMethod } from "./index"
|
||||
import { addRemoteMcpServer } from "./addRemoteMcpServer"
|
||||
import { downloadMcp } from "./downloadMcp"
|
||||
import { restartMcpServer } from "./restartMcpServer"
|
||||
import { toggleMcpServer } from "./toggleMcpServer"
|
||||
import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
|
||||
@@ -13,6 +14,7 @@ export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
|
||||
registerMethod("downloadMcp", downloadMcp)
|
||||
registerMethod("restartMcpServer", restartMcpServer)
|
||||
registerMethod("toggleMcpServer", toggleMcpServer)
|
||||
registerMethod("updateMcpTimeout", updateMcpTimeout)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { McpServers } from "@shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
|
||||
/**
|
||||
* Restarts an MCP server connection
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the server name
|
||||
* @returns The updated list of MCP servers
|
||||
*/
|
||||
export async function restartMcpServer(controller: Controller, request: StringRequest): Promise<McpServers> {
|
||||
try {
|
||||
const mcpServers = await controller.mcpHub?.restartConnectionRPC(request.value)
|
||||
|
||||
// Convert from McpServer[] to ProtoMcpServer[] ensuring all required fields are set
|
||||
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart MCP server ${request.value}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@ export class McpHub {
|
||||
|
||||
getServers(): McpServer[] {
|
||||
// Only return enabled servers
|
||||
|
||||
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
|
||||
}
|
||||
|
||||
@@ -180,6 +181,120 @@ export class McpHub {
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServerRPC(
|
||||
name: string,
|
||||
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
|
||||
): Promise<void> {
|
||||
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
|
||||
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.clientVersion,
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
},
|
||||
)
|
||||
|
||||
let transport: StdioClientTransport | SSEClientTransport
|
||||
|
||||
if (config.transportType === "sse") {
|
||||
transport = new SSEClientTransport(new URL(config.url), {})
|
||||
} else {
|
||||
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 } : {}),
|
||||
},
|
||||
stderr: "pipe", // necessary for stderr to be available
|
||||
})
|
||||
}
|
||||
|
||||
transport.onerror = async (error) => {
|
||||
console.error(`Transport error for "${name}":`, error)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
transport.onclose = async () => {
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
}
|
||||
}
|
||||
|
||||
const connection: McpConnection = {
|
||||
server: {
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "connecting",
|
||||
disabled: config.disabled,
|
||||
},
|
||||
client,
|
||||
transport,
|
||||
}
|
||||
this.connections.push(connection)
|
||||
|
||||
if (config.transportType === "stdio") {
|
||||
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
|
||||
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
|
||||
await transport.start()
|
||||
const stderrStream = (transport as StdioClientTransport).stderr
|
||||
if (stderrStream) {
|
||||
stderrStream.on("data", async (data: Buffer) => {
|
||||
const output = data.toString()
|
||||
// Check if output contains INFO level log
|
||||
const isInfoLog = !/\berror\b/i.test(output)
|
||||
|
||||
if (isInfoLog) {
|
||||
// Log normal informational messages
|
||||
console.info(`Server "${name}" info:`, output)
|
||||
} else {
|
||||
// Treat as error log
|
||||
console.error(`Server "${name}" stderr:`, output)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
this.appendErrorMessage(connection, output)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error(`No stderr stream for ${name}`)
|
||||
}
|
||||
transport.start = async () => {} // No-op now, .connect() won't fail
|
||||
}
|
||||
|
||||
// Connect
|
||||
await client.connect(transport)
|
||||
|
||||
connection.server.status = "connected"
|
||||
connection.server.error = ""
|
||||
|
||||
// Initial fetch of tools and resources
|
||||
connection.server.tools = await this.fetchToolsList(name)
|
||||
connection.server.resources = await this.fetchResourcesList(name)
|
||||
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
|
||||
} catch (error) {
|
||||
// Update status with error
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServer(
|
||||
name: string,
|
||||
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
|
||||
@@ -494,6 +609,36 @@ export class McpHub {
|
||||
this.fileWatchers.clear()
|
||||
}
|
||||
|
||||
async restartConnectionRPC(serverName: string): Promise<McpServer[]> {
|
||||
this.isConnecting = true
|
||||
|
||||
// Get existing connection and update its status
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
const inMemoryConfig = connection?.server.config
|
||||
if (inMemoryConfig) {
|
||||
connection.server.status = "connecting"
|
||||
connection.server.error = ""
|
||||
await setTimeoutPromise(500) // artificial delay to show user that server is restarting
|
||||
try {
|
||||
await this.deleteConnection(serverName)
|
||||
// Try to connect again using existing config
|
||||
await this.connectToServerRPC(serverName, JSON.parse(inMemoryConfig))
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart connection for ${serverName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
this.isConnecting = false
|
||||
|
||||
const config = await this.readAndValidateMcpSettingsFile()
|
||||
if (!config) {
|
||||
throw new Error("Failed to read or validate MCP settings")
|
||||
}
|
||||
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
}
|
||||
|
||||
async restartConnection(serverName: string): Promise<void> {
|
||||
this.isConnecting = true
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface WebviewMessage {
|
||||
| "showChatView"
|
||||
| "refreshClineRules"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserRelaunchResult"
|
||||
|
||||
@@ -915,15 +915,6 @@ export const FileServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Select images from the file system and return as data URLs */
|
||||
selectImages: {
|
||||
name: "selectImages",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: StringArray,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Opens an image in the system viewer */
|
||||
openImage: {
|
||||
name: "openImage",
|
||||
@@ -960,6 +951,15 @@ export const FileServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Select images from the file system and return as data URLs */
|
||||
selectImages: {
|
||||
name: "selectImages",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: StringArray,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Convert URIs to workspace-relative paths */
|
||||
getRelativePaths: {
|
||||
name: "getRelativePaths",
|
||||
|
||||
@@ -1012,6 +1012,14 @@ export const McpServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
restartMcpServer: {
|
||||
name: "restartMcpServer",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: McpServers,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ import { checkpointRestore } from "../core/controller/checkpoints/checkpointRest
|
||||
|
||||
// File Service
|
||||
import { openFile } from "../core/controller/file/openFile"
|
||||
import { selectImages } from "../core/controller/file/selectImages"
|
||||
import { openImage } from "../core/controller/file/openImage"
|
||||
import { deleteRuleFile } from "../core/controller/file/deleteRuleFile"
|
||||
import { createRuleFile } from "../core/controller/file/createRuleFile"
|
||||
import { searchCommits } from "../core/controller/file/searchCommits"
|
||||
import { selectImages } from "../core/controller/file/selectImages"
|
||||
import { getRelativePaths } from "../core/controller/file/getRelativePaths"
|
||||
import { searchFiles } from "../core/controller/file/searchFiles"
|
||||
|
||||
@@ -33,6 +33,7 @@ import { toggleMcpServer } from "../core/controller/mcp/toggleMcpServer"
|
||||
import { updateMcpTimeout } from "../core/controller/mcp/updateMcpTimeout"
|
||||
import { addRemoteMcpServer } from "../core/controller/mcp/addRemoteMcpServer"
|
||||
import { downloadMcp } from "../core/controller/mcp/downloadMcp"
|
||||
import { restartMcpServer } from "../core/controller/mcp/restartMcpServer"
|
||||
|
||||
// Models Service
|
||||
import { getOllamaModels } from "../core/controller/models/getOllamaModels"
|
||||
@@ -96,11 +97,11 @@ export function addServices(
|
||||
// File Service
|
||||
server.addService(proto.cline.FileService.service, {
|
||||
openFile: wrapper(openFile, controller),
|
||||
selectImages: wrapper(selectImages, controller),
|
||||
openImage: wrapper(openImage, controller),
|
||||
deleteRuleFile: wrapper(deleteRuleFile, controller),
|
||||
createRuleFile: wrapper(createRuleFile, controller),
|
||||
searchCommits: wrapper(searchCommits, controller),
|
||||
selectImages: wrapper(selectImages, controller),
|
||||
getRelativePaths: wrapper(getRelativePaths, controller),
|
||||
searchFiles: wrapper(searchFiles, controller),
|
||||
})
|
||||
@@ -111,6 +112,7 @@ export function addServices(
|
||||
updateMcpTimeout: wrapper(updateMcpTimeout, controller),
|
||||
addRemoteMcpServer: wrapper(addRemoteMcpServer, controller),
|
||||
downloadMcp: wrapper(downloadMcp, controller),
|
||||
restartMcpServer: wrapper(restartMcpServer, controller),
|
||||
})
|
||||
|
||||
// Models Service
|
||||
|
||||
+24
-8
@@ -19,6 +19,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { McpServiceClient } from "@/services/grpc-client"
|
||||
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { McpServers, UpdateMcpTimeoutRequest } from "@shared/proto/mcp"
|
||||
import { StringRequest } from "@shared/proto/common"
|
||||
// constant JSX.Elements
|
||||
const TimeoutOptions = [
|
||||
{ value: "30", label: "30 seconds" },
|
||||
@@ -46,6 +47,7 @@ const ServerRow = ({
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [isRestarting, setIsRestarting] = useState(false)
|
||||
|
||||
const getStatusColor = useCallback((status: McpServer["status"]) => {
|
||||
switch (status) {
|
||||
@@ -93,10 +95,24 @@ const ServerRow = ({
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
vscode.postMessage({
|
||||
type: "restartMcpServer",
|
||||
text: server.name,
|
||||
})
|
||||
// Set local state to show "connecting" status
|
||||
setIsRestarting(true)
|
||||
|
||||
// Make the gRPC call
|
||||
McpServiceClient.restartMcpServer({
|
||||
value: server.name,
|
||||
} as StringRequest)
|
||||
.then((response: McpServers) => {
|
||||
// Update with the final state from the server
|
||||
const mcpServers = convertProtoMcpServersToMcpServers(response.mcpServers)
|
||||
setMcpServers(mcpServers)
|
||||
setIsRestarting(false)
|
||||
})
|
||||
.catch((error) => {
|
||||
// Reset the restarting state
|
||||
setIsRestarting(false)
|
||||
console.error("Error restarting MCP server", error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
@@ -171,7 +187,7 @@ const ServerRow = ({
|
||||
e.stopPropagation()
|
||||
handleRestart()
|
||||
}}
|
||||
disabled={server.status === "connecting"}>
|
||||
disabled={server.status === "connecting" || isRestarting}>
|
||||
<span className="codicon codicon-sync"></span>
|
||||
</VSCodeButton>
|
||||
{hasTrashIcon && (
|
||||
@@ -267,7 +283,7 @@ const ServerRow = ({
|
||||
width: "calc(100% - 20px)",
|
||||
margin: "0 10px 10px 10px",
|
||||
}}>
|
||||
{server.status === "connecting" ? "Retrying..." : "Retry Connection"}
|
||||
{server.status === "connecting" || isRestarting ? "Retrying..." : "Retry Connection"}
|
||||
</VSCodeButton>
|
||||
|
||||
<DangerButton
|
||||
@@ -363,12 +379,12 @@ const ServerRow = ({
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleRestart}
|
||||
disabled={server.status === "connecting"}
|
||||
disabled={server.status === "connecting" || isRestarting}
|
||||
style={{
|
||||
width: "calc(100% - 14px)",
|
||||
margin: "0 7px 3px 7px",
|
||||
}}>
|
||||
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
|
||||
{server.status === "connecting" || isRestarting ? "Restarting..." : "Restart Server"}
|
||||
</VSCodeButton>
|
||||
|
||||
<DangerButton
|
||||
|
||||
Reference in New Issue
Block a user