PROTOBUS: toggleMcpServer (#3063)

* wip

* migrate toggleMcpServer

* changeset

* support optional types and enum type
This commit is contained in:
Evan 2025-04-22 16:29:13 -07:00 committed by GitHub
parent 4af5150823
commit dfcb3d5d9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1143 additions and 30 deletions

View File

@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate the toggleMcpServer message to protobus

View File

@ -114,6 +114,7 @@ async function generateMethodRegistrations() {
console.log(chalk.cyan("Generating method registration files..."))
const serviceDirs = [
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
path.join(ROOT_DIR, "src", "core", "controller", "browser"),
path.join(ROOT_DIR, "src", "core", "controller", "checkpoints"),
// Add more service directories here as needed

60
proto/mcp.proto Normal file
View File

@ -0,0 +1,60 @@
syntax = "proto3";
package cline;
import "common.proto";
service McpService {
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
}
message McpTool {
string name = 1;
optional string description = 2;
optional string input_schema = 3;
optional bool auto_approve = 4;
}
message McpResource {
string uri = 1;
string name = 2;
optional string mime_type = 3;
optional string description = 4;
}
message McpResourceTemplate {
string uri_template = 1;
string name = 2;
optional string mime_type = 3;
optional string description = 4;
}
enum McpServerStatus {
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
// To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
MCP_SERVER_STATUS_DISCONNECTED = 0; // default
MCP_SERVER_STATUS_CONNECTED = 1;
MCP_SERVER_STATUS_CONNECTING = 2;
}
message McpServer {
string name = 1;
string config = 2;
McpServerStatus status = 3;
optional string error = 4;
repeated McpTool tools = 5;
repeated McpResource resources = 6;
repeated McpResourceTemplate resource_templates = 7;
optional bool disabled = 8;
optional int32 timeout = 9;
}
message ToggleMcpServerRequest {
Metadata metadata = 1;
string server_name = 2;
bool disabled = 3;
}
message McpServers {
repeated McpServer mcp_servers = 1;
}

View File

@ -1,6 +1,7 @@
import { Controller } from "./index"
import { handleBrowserServiceRequest } from "./browser/index"
import { handleCheckpointsDiffServiceRequest } from "./checkpoints"
import { handleMcpServiceRequest } from "./mcp"
/**
* Handles gRPC requests from the webview
@ -38,6 +39,11 @@ export class GrpcHandler {
message: await handleCheckpointsDiffServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.McpService":
return {
message: await handleMcpServiceRequest(this.controller, method, message),
request_id: requestId,
}
default:
throw new Error(`Unknown service: ${service}`)
}

View File

@ -588,14 +588,6 @@ export class Controller {
// break
// }
case "toggleMcpServer": {
try {
await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!)
} catch (error) {
console.error(`Failed to toggle MCP server ${message.serverName}:`, error)
}
break
}
case "toggleToolAutoApprove": {
try {
await this.mcpHub?.toggleToolAutoApprove(message.serverName!, message.toolNames!, message.autoApprove!)

View File

@ -0,0 +1,15 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
import { registerAllMethods } from "./methods"
// Create MCP service registry
const mcpService = createServiceRegistry("mcp")
// Export the method handler type and registration function
export type McpMethodHandler = ServiceMethodHandler
export const registerMethod = mcpService.registerMethod
// Export the request handler
export const handleMcpServiceRequest = mcpService.handleRequest
// Register all mcp methods
registerAllMethods()

View File

@ -0,0 +1,12 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { toggleMcpServer } from "./toggleMcpServer"
// Register all mcp service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("toggleMcpServer", toggleMcpServer)
}

View File

@ -0,0 +1,23 @@
import type { ToggleMcpServerRequest, McpServers } from "../../../shared/proto/mcp"
import type { Controller } from "../index"
import { convertMcpServersToProtoMcpServers } from "../../../shared/proto-conversions/mcp/mcp-server-conversion"
/**
* Toggles an MCP server's enabled/disabled status
* @param controller The controller instance
* @param request The request containing server ID and disabled status
* @returns A response indicating success or failure
*/
export async function toggleMcpServer(controller: Controller, request: ToggleMcpServerRequest): Promise<McpServers> {
try {
const mcpServers = await controller.mcpHub?.toggleServerDisabledRPC(request.serverName, request.disabled)
// Convert from McpServer[] to ProtoMcpServer[] ensuring all required fields are set
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
return { mcpServers: protoServers }
} catch (error) {
console.error(`Failed to toggle MCP server ${request.serverName}:`, error)
throw error
}
}

View File

@ -501,7 +501,7 @@ export class McpHub {
// Public methods for server management
public async toggleServerDisabled(serverName: string, disabled: boolean): Promise<void> {
public async toggleServerDisabledRPC(serverName: string, disabled: boolean): Promise<McpServer[]> {
try {
const config = await this.readAndValidateMcpSettingsFile()
if (!config) {
@ -519,8 +519,20 @@ export class McpHub {
connection.server.disabled = disabled
}
await this.notifyWebviewOfServerChanges()
const serverOrder = Object.keys(config.mcpServers || {})
const mcpServers = [...this.connections]
.sort((a, b) => {
const indexA = serverOrder.indexOf(a.server.name)
const indexB = serverOrder.indexOf(b.server.name)
return indexA - indexB
})
.map((connection) => connection.server)
return mcpServers
}
console.error(`Server "${serverName}" not found in MCP configuration`)
throw new Error(`Server "${serverName}" not found in MCP configuration`)
} catch (error) {
console.error("Failed to update server disabled state:", error)
if (error instanceof Error) {

View File

@ -0,0 +1,153 @@
import { McpServer, McpTool, McpResource, McpResourceTemplate } from "../../mcp"
import {
McpServer as ProtoMcpServer,
McpTool as ProtoMcpTool,
McpResource as ProtoMcpResource,
McpResourceTemplate as ProtoMcpResourceTemplate,
McpServerStatus,
} from "../../proto/mcp"
// Helper to convert TS status to Proto enum
function convertMcpStatusToProto(status: McpServer["status"]): McpServerStatus {
switch (status) {
case "connected":
return McpServerStatus.MCP_SERVER_STATUS_CONNECTED
case "connecting":
return McpServerStatus.MCP_SERVER_STATUS_CONNECTING
case "disconnected":
return McpServerStatus.MCP_SERVER_STATUS_DISCONNECTED
}
}
export function convertMcpServersToProtoMcpServers(mcpServers: McpServer[]): ProtoMcpServer[] {
const protoServers: ProtoMcpServer[] = mcpServers.map((server) => ({
name: server.name,
config: server.config,
status: convertMcpStatusToProto(server.status),
error: server.error,
// Convert nested types
tools: (server.tools || []).map(convertTool),
resources: (server.resources || []).map(convertResource),
resourceTemplates: (server.resourceTemplates || []).map(convertResourceTemplate),
disabled: server.disabled,
timeout: server.timeout,
}))
return protoServers
}
/**
* Converts McpTool to ProtoMcpTool format, ensuring all required fields have values
*/
function convertTool(tool: McpTool): ProtoMcpTool {
const inputSchemaString = tool.inputSchema
? typeof tool.inputSchema === "object"
? JSON.stringify(tool.inputSchema)
: tool.inputSchema
: undefined
return {
name: tool.name,
description: tool.description,
inputSchema: inputSchemaString,
autoApprove: tool.autoApprove,
}
}
/**
* Converts McpResource to ProtoMcpResource format, ensuring all required fields have values
*/
function convertResource(resource: McpResource): ProtoMcpResource {
return {
uri: resource.uri,
name: resource.name,
mimeType: resource.mimeType,
description: resource.description,
}
}
/**
* Converts McpResourceTemplate to ProtoMcpResourceTemplate format, ensuring all required fields have values
*/
function convertResourceTemplate(template: McpResourceTemplate): ProtoMcpResourceTemplate {
return {
uriTemplate: template.uriTemplate,
name: template.name,
mimeType: template.mimeType,
description: template.description,
}
}
// Helper to convert Proto enum to TS status
function convertProtoStatusToMcp(status: McpServerStatus): McpServer["status"] {
switch (status) {
case McpServerStatus.MCP_SERVER_STATUS_CONNECTED:
return "connected"
case McpServerStatus.MCP_SERVER_STATUS_CONNECTING:
return "connecting"
case McpServerStatus.MCP_SERVER_STATUS_DISCONNECTED:
default: // Includes UNSPECIFIED if it were present, maps to disconnected
return "disconnected"
}
}
export function convertProtoMcpServersToMcpServers(protoServers: ProtoMcpServer[]): McpServer[] {
const mcpServers: McpServer[] = protoServers.map((protoServer) => {
return {
name: protoServer.name,
config: protoServer.config,
status: convertProtoStatusToMcp(protoServer.status),
error: protoServer.error === "" ? undefined : protoServer.error,
// Convert nested types
tools: protoServer.tools.map(convertProtoTool),
resources: protoServer.resources.map(convertProtoResource),
resourceTemplates: protoServer.resourceTemplates.map(convertProtoResourceTemplate),
disabled: protoServer.disabled,
timeout: protoServer.timeout,
}
})
return mcpServers
}
/**
* Converts ProtoMcpTool to McpTool format, parsing inputSchema if needed
*/
function convertProtoTool(protoTool: ProtoMcpTool): McpTool {
return {
name: protoTool.name,
description: protoTool.description === "" ? undefined : protoTool.description,
inputSchema: protoTool.inputSchema
? protoTool.inputSchema.startsWith("{")
? JSON.parse(protoTool.inputSchema)
: protoTool.inputSchema
: undefined,
autoApprove: protoTool.autoApprove,
}
}
/**
* Converts ProtoMcpResource to McpResource format
*/
function convertProtoResource(protoResource: ProtoMcpResource): McpResource {
return {
uri: protoResource.uri,
name: protoResource.name,
mimeType: protoResource.mimeType === "" ? undefined : protoResource.mimeType,
description: protoResource.description === "" ? undefined : protoResource.description,
}
}
/**
* Converts ProtoMcpResourceTemplate to McpResourceTemplate format
*/
function convertProtoResourceTemplate(protoTemplate: ProtoMcpResourceTemplate): McpResourceTemplate {
return {
uriTemplate: protoTemplate.uriTemplate,
name: protoTemplate.name,
mimeType: protoTemplate.mimeType === "" ? undefined : protoTemplate.mimeType,
description: protoTemplate.description === "" ? undefined : protoTemplate.description,
}
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.1
// protoc v6.30.2
// source: browser.proto
/* eslint-disable */

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.1
// protoc v6.30.2
// source: checkpoints.proto
/* eslint-disable */

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.1
// protoc v6.30.2
// source: common.proto
/* eslint-disable */

824
src/shared/proto/mcp.ts Normal file
View File

@ -0,0 +1,824 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.2
// source: mcp.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { Metadata } from "./common"
export const protobufPackage = "cline"
export enum McpServerStatus {
/**
* MCP_SERVER_STATUS_DISCONNECTED - Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
* To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
*/
MCP_SERVER_STATUS_DISCONNECTED = 0,
MCP_SERVER_STATUS_CONNECTED = 1,
MCP_SERVER_STATUS_CONNECTING = 2,
UNRECOGNIZED = -1,
}
export function mcpServerStatusFromJSON(object: any): McpServerStatus {
switch (object) {
case 0:
case "MCP_SERVER_STATUS_DISCONNECTED":
return McpServerStatus.MCP_SERVER_STATUS_DISCONNECTED
case 1:
case "MCP_SERVER_STATUS_CONNECTED":
return McpServerStatus.MCP_SERVER_STATUS_CONNECTED
case 2:
case "MCP_SERVER_STATUS_CONNECTING":
return McpServerStatus.MCP_SERVER_STATUS_CONNECTING
case -1:
case "UNRECOGNIZED":
default:
return McpServerStatus.UNRECOGNIZED
}
}
export function mcpServerStatusToJSON(object: McpServerStatus): string {
switch (object) {
case McpServerStatus.MCP_SERVER_STATUS_DISCONNECTED:
return "MCP_SERVER_STATUS_DISCONNECTED"
case McpServerStatus.MCP_SERVER_STATUS_CONNECTED:
return "MCP_SERVER_STATUS_CONNECTED"
case McpServerStatus.MCP_SERVER_STATUS_CONNECTING:
return "MCP_SERVER_STATUS_CONNECTING"
case McpServerStatus.UNRECOGNIZED:
default:
return "UNRECOGNIZED"
}
}
export interface McpTool {
name: string
description?: string | undefined
inputSchema?: string | undefined
autoApprove?: boolean | undefined
}
export interface McpResource {
uri: string
name: string
mimeType?: string | undefined
description?: string | undefined
}
export interface McpResourceTemplate {
uriTemplate: string
name: string
mimeType?: string | undefined
description?: string | undefined
}
export interface McpServer {
name: string
config: string
status: McpServerStatus
error?: string | undefined
tools: McpTool[]
resources: McpResource[]
resourceTemplates: McpResourceTemplate[]
disabled?: boolean | undefined
timeout?: number | undefined
}
export interface ToggleMcpServerRequest {
metadata?: Metadata | undefined
serverName: string
disabled: boolean
}
export interface McpServers {
mcpServers: McpServer[]
}
function createBaseMcpTool(): McpTool {
return { name: "", description: undefined, inputSchema: undefined, autoApprove: undefined }
}
export const McpTool: MessageFns<McpTool> = {
encode(message: McpTool, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.name !== "") {
writer.uint32(10).string(message.name)
}
if (message.description !== undefined) {
writer.uint32(18).string(message.description)
}
if (message.inputSchema !== undefined) {
writer.uint32(26).string(message.inputSchema)
}
if (message.autoApprove !== undefined) {
writer.uint32(32).bool(message.autoApprove)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): McpTool {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseMcpTool()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.name = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.description = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.inputSchema = reader.string()
continue
}
case 4: {
if (tag !== 32) {
break
}
message.autoApprove = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): McpTool {
return {
name: isSet(object.name) ? globalThis.String(object.name) : "",
description: isSet(object.description) ? globalThis.String(object.description) : undefined,
inputSchema: isSet(object.inputSchema) ? globalThis.String(object.inputSchema) : undefined,
autoApprove: isSet(object.autoApprove) ? globalThis.Boolean(object.autoApprove) : undefined,
}
},
toJSON(message: McpTool): unknown {
const obj: any = {}
if (message.name !== "") {
obj.name = message.name
}
if (message.description !== undefined) {
obj.description = message.description
}
if (message.inputSchema !== undefined) {
obj.inputSchema = message.inputSchema
}
if (message.autoApprove !== undefined) {
obj.autoApprove = message.autoApprove
}
return obj
},
create<I extends Exact<DeepPartial<McpTool>, I>>(base?: I): McpTool {
return McpTool.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<McpTool>, I>>(object: I): McpTool {
const message = createBaseMcpTool()
message.name = object.name ?? ""
message.description = object.description ?? undefined
message.inputSchema = object.inputSchema ?? undefined
message.autoApprove = object.autoApprove ?? undefined
return message
},
}
function createBaseMcpResource(): McpResource {
return { uri: "", name: "", mimeType: undefined, description: undefined }
}
export const McpResource: MessageFns<McpResource> = {
encode(message: McpResource, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.uri !== "") {
writer.uint32(10).string(message.uri)
}
if (message.name !== "") {
writer.uint32(18).string(message.name)
}
if (message.mimeType !== undefined) {
writer.uint32(26).string(message.mimeType)
}
if (message.description !== undefined) {
writer.uint32(34).string(message.description)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): McpResource {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseMcpResource()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.uri = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.name = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.mimeType = reader.string()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.description = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): McpResource {
return {
uri: isSet(object.uri) ? globalThis.String(object.uri) : "",
name: isSet(object.name) ? globalThis.String(object.name) : "",
mimeType: isSet(object.mimeType) ? globalThis.String(object.mimeType) : undefined,
description: isSet(object.description) ? globalThis.String(object.description) : undefined,
}
},
toJSON(message: McpResource): unknown {
const obj: any = {}
if (message.uri !== "") {
obj.uri = message.uri
}
if (message.name !== "") {
obj.name = message.name
}
if (message.mimeType !== undefined) {
obj.mimeType = message.mimeType
}
if (message.description !== undefined) {
obj.description = message.description
}
return obj
},
create<I extends Exact<DeepPartial<McpResource>, I>>(base?: I): McpResource {
return McpResource.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<McpResource>, I>>(object: I): McpResource {
const message = createBaseMcpResource()
message.uri = object.uri ?? ""
message.name = object.name ?? ""
message.mimeType = object.mimeType ?? undefined
message.description = object.description ?? undefined
return message
},
}
function createBaseMcpResourceTemplate(): McpResourceTemplate {
return { uriTemplate: "", name: "", mimeType: undefined, description: undefined }
}
export const McpResourceTemplate: MessageFns<McpResourceTemplate> = {
encode(message: McpResourceTemplate, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.uriTemplate !== "") {
writer.uint32(10).string(message.uriTemplate)
}
if (message.name !== "") {
writer.uint32(18).string(message.name)
}
if (message.mimeType !== undefined) {
writer.uint32(26).string(message.mimeType)
}
if (message.description !== undefined) {
writer.uint32(34).string(message.description)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): McpResourceTemplate {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseMcpResourceTemplate()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.uriTemplate = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.name = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.mimeType = reader.string()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.description = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): McpResourceTemplate {
return {
uriTemplate: isSet(object.uriTemplate) ? globalThis.String(object.uriTemplate) : "",
name: isSet(object.name) ? globalThis.String(object.name) : "",
mimeType: isSet(object.mimeType) ? globalThis.String(object.mimeType) : undefined,
description: isSet(object.description) ? globalThis.String(object.description) : undefined,
}
},
toJSON(message: McpResourceTemplate): unknown {
const obj: any = {}
if (message.uriTemplate !== "") {
obj.uriTemplate = message.uriTemplate
}
if (message.name !== "") {
obj.name = message.name
}
if (message.mimeType !== undefined) {
obj.mimeType = message.mimeType
}
if (message.description !== undefined) {
obj.description = message.description
}
return obj
},
create<I extends Exact<DeepPartial<McpResourceTemplate>, I>>(base?: I): McpResourceTemplate {
return McpResourceTemplate.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<McpResourceTemplate>, I>>(object: I): McpResourceTemplate {
const message = createBaseMcpResourceTemplate()
message.uriTemplate = object.uriTemplate ?? ""
message.name = object.name ?? ""
message.mimeType = object.mimeType ?? undefined
message.description = object.description ?? undefined
return message
},
}
function createBaseMcpServer(): McpServer {
return {
name: "",
config: "",
status: 0,
error: undefined,
tools: [],
resources: [],
resourceTemplates: [],
disabled: undefined,
timeout: undefined,
}
}
export const McpServer: MessageFns<McpServer> = {
encode(message: McpServer, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.name !== "") {
writer.uint32(10).string(message.name)
}
if (message.config !== "") {
writer.uint32(18).string(message.config)
}
if (message.status !== 0) {
writer.uint32(24).int32(message.status)
}
if (message.error !== undefined) {
writer.uint32(34).string(message.error)
}
for (const v of message.tools) {
McpTool.encode(v!, writer.uint32(42).fork()).join()
}
for (const v of message.resources) {
McpResource.encode(v!, writer.uint32(50).fork()).join()
}
for (const v of message.resourceTemplates) {
McpResourceTemplate.encode(v!, writer.uint32(58).fork()).join()
}
if (message.disabled !== undefined) {
writer.uint32(64).bool(message.disabled)
}
if (message.timeout !== undefined) {
writer.uint32(72).int32(message.timeout)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): McpServer {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseMcpServer()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.name = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.config = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.status = reader.int32() as any
continue
}
case 4: {
if (tag !== 34) {
break
}
message.error = reader.string()
continue
}
case 5: {
if (tag !== 42) {
break
}
message.tools.push(McpTool.decode(reader, reader.uint32()))
continue
}
case 6: {
if (tag !== 50) {
break
}
message.resources.push(McpResource.decode(reader, reader.uint32()))
continue
}
case 7: {
if (tag !== 58) {
break
}
message.resourceTemplates.push(McpResourceTemplate.decode(reader, reader.uint32()))
continue
}
case 8: {
if (tag !== 64) {
break
}
message.disabled = reader.bool()
continue
}
case 9: {
if (tag !== 72) {
break
}
message.timeout = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): McpServer {
return {
name: isSet(object.name) ? globalThis.String(object.name) : "",
config: isSet(object.config) ? globalThis.String(object.config) : "",
status: isSet(object.status) ? mcpServerStatusFromJSON(object.status) : 0,
error: isSet(object.error) ? globalThis.String(object.error) : undefined,
tools: globalThis.Array.isArray(object?.tools) ? object.tools.map((e: any) => McpTool.fromJSON(e)) : [],
resources: globalThis.Array.isArray(object?.resources)
? object.resources.map((e: any) => McpResource.fromJSON(e))
: [],
resourceTemplates: globalThis.Array.isArray(object?.resourceTemplates)
? object.resourceTemplates.map((e: any) => McpResourceTemplate.fromJSON(e))
: [],
disabled: isSet(object.disabled) ? globalThis.Boolean(object.disabled) : undefined,
timeout: isSet(object.timeout) ? globalThis.Number(object.timeout) : undefined,
}
},
toJSON(message: McpServer): unknown {
const obj: any = {}
if (message.name !== "") {
obj.name = message.name
}
if (message.config !== "") {
obj.config = message.config
}
if (message.status !== 0) {
obj.status = mcpServerStatusToJSON(message.status)
}
if (message.error !== undefined) {
obj.error = message.error
}
if (message.tools?.length) {
obj.tools = message.tools.map((e) => McpTool.toJSON(e))
}
if (message.resources?.length) {
obj.resources = message.resources.map((e) => McpResource.toJSON(e))
}
if (message.resourceTemplates?.length) {
obj.resourceTemplates = message.resourceTemplates.map((e) => McpResourceTemplate.toJSON(e))
}
if (message.disabled !== undefined) {
obj.disabled = message.disabled
}
if (message.timeout !== undefined) {
obj.timeout = Math.round(message.timeout)
}
return obj
},
create<I extends Exact<DeepPartial<McpServer>, I>>(base?: I): McpServer {
return McpServer.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<McpServer>, I>>(object: I): McpServer {
const message = createBaseMcpServer()
message.name = object.name ?? ""
message.config = object.config ?? ""
message.status = object.status ?? 0
message.error = object.error ?? undefined
message.tools = object.tools?.map((e) => McpTool.fromPartial(e)) || []
message.resources = object.resources?.map((e) => McpResource.fromPartial(e)) || []
message.resourceTemplates = object.resourceTemplates?.map((e) => McpResourceTemplate.fromPartial(e)) || []
message.disabled = object.disabled ?? undefined
message.timeout = object.timeout ?? undefined
return message
},
}
function createBaseToggleMcpServerRequest(): ToggleMcpServerRequest {
return { metadata: undefined, serverName: "", disabled: false }
}
export const ToggleMcpServerRequest: MessageFns<ToggleMcpServerRequest> = {
encode(message: ToggleMcpServerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.serverName !== "") {
writer.uint32(18).string(message.serverName)
}
if (message.disabled !== false) {
writer.uint32(24).bool(message.disabled)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): ToggleMcpServerRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseToggleMcpServerRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.serverName = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.disabled = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): ToggleMcpServerRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
disabled: isSet(object.disabled) ? globalThis.Boolean(object.disabled) : false,
}
},
toJSON(message: ToggleMcpServerRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.serverName !== "") {
obj.serverName = message.serverName
}
if (message.disabled !== false) {
obj.disabled = message.disabled
}
return obj
},
create<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(base?: I): ToggleMcpServerRequest {
return ToggleMcpServerRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(object: I): ToggleMcpServerRequest {
const message = createBaseToggleMcpServerRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.serverName = object.serverName ?? ""
message.disabled = object.disabled ?? false
return message
},
}
function createBaseMcpServers(): McpServers {
return { mcpServers: [] }
}
export const McpServers: MessageFns<McpServers> = {
encode(message: McpServers, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.mcpServers) {
McpServer.encode(v!, writer.uint32(10).fork()).join()
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): McpServers {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseMcpServers()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.mcpServers.push(McpServer.decode(reader, reader.uint32()))
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): McpServers {
return {
mcpServers: globalThis.Array.isArray(object?.mcpServers)
? object.mcpServers.map((e: any) => McpServer.fromJSON(e))
: [],
}
},
toJSON(message: McpServers): unknown {
const obj: any = {}
if (message.mcpServers?.length) {
obj.mcpServers = message.mcpServers.map((e) => McpServer.toJSON(e))
}
return obj
},
create<I extends Exact<DeepPartial<McpServers>, I>>(base?: I): McpServers {
return McpServers.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<McpServers>, I>>(object: I): McpServers {
const message = createBaseMcpServers()
message.mcpServers = object.mcpServers?.map((e) => McpServer.fromPartial(e)) || []
return message
},
}
export type McpServiceDefinition = typeof McpServiceDefinition
export const McpServiceDefinition = {
name: "McpService",
fullName: "cline.McpService",
methods: {
toggleMcpServer: {
name: "toggleMcpServer",
requestType: ToggleMcpServerRequest,
requestStream: false,
responseType: McpServers,
responseStream: false,
options: {},
},
},
} as const
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}

View File

@ -41,7 +41,7 @@ const McpToolRow = ({ tool, serverName }: McpToolRowProps) => {
<span style={{ fontWeight: 500 }}>{tool.name}</span>
</div>
{serverName && autoApprovalSettings.enabled && autoApprovalSettings.actions.useMcp && (
<VSCodeCheckbox checked={tool.autoApprove} onChange={handleAutoApproveChange} data-tool={tool.name}>
<VSCodeCheckbox checked={tool.autoApprove ?? false} onChange={handleAutoApproveChange} data-tool={tool.name}>
Auto-approve
</VSCodeCheckbox>
)}

View File

@ -1,6 +1,6 @@
import { McpServer } from "@shared/mcp"
import { DEFAULT_MCP_TIMEOUT_SECONDS } from "@shared/mcp"
import { useState, useCallback } from "react"
import { useState, useCallback, useEffect } from "react"
import { vscode } from "@/utils/vscode"
import {
VSCodeButton,
@ -16,7 +16,8 @@ import DangerButton from "@/components/common/DangerButton"
import McpToolRow from "./McpToolRow"
import McpResourceRow from "./McpResourceRow"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { McpServiceClient } from "@/services/grpc-client"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
// constant JSX.Elements
const TimeoutOptions = [
{ value: "30", label: "30 seconds" },
@ -40,7 +41,7 @@ const ServerRow = ({
isExpandable?: boolean
hasTrashIcon?: boolean
}) => {
const { mcpMarketplaceCatalog, autoApprovalSettings } = useExtensionState()
const { mcpMarketplaceCatalog, autoApprovalSettings, setMcpServers } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
@ -109,6 +110,20 @@ const ServerRow = ({
})
}
const handleToggleMcpServer = () => {
McpServiceClient.toggleMcpServer({
serverName: server.name,
disabled: !server.disabled,
})
.then((response) => {
const mcpServers = convertProtoMcpServersToMcpServers(response.mcpServers)
setMcpServers(mcpServers)
})
.catch((error) => {
console.error("Error toggling MCP server", error)
})
}
return (
<div style={{ marginBottom: "10px" }}>
<div
@ -184,20 +199,12 @@ const ServerRow = ({
opacity: server.disabled ? 0.5 : 0.9,
}}
onClick={() => {
vscode.postMessage({
type: "toggleMcpServer",
serverName: server.name,
disabled: !server.disabled,
})
handleToggleMcpServer()
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
vscode.postMessage({
type: "toggleMcpServer",
serverName: server.name,
disabled: !server.disabled,
})
handleToggleMcpServer()
}
}}>
<div

View File

@ -17,7 +17,7 @@ import { vscode } from "../utils/vscode"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { ClineRulesToggles } from "@shared/cline-rules"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
@ -34,6 +34,7 @@ interface ExtensionStateContextType extends ExtensionState {
setTelemetrySetting: (value: TelemetrySetting) => void
setShowAnnouncement: (value: boolean) => void
setPlanActSeparateModelsSetting: (value: boolean) => void
setMcpServers: (value: McpServer[]) => void
}
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@ -226,6 +227,7 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
shouldShowAnnouncement: value,
})),
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

View File

@ -3,7 +3,7 @@ import { v4 as uuidv4 } from "uuid"
import { BrowserServiceDefinition } from "@shared/proto/browser"
import { CheckpointsServiceDefinition } from "@shared/proto/checkpoints"
import { EmptyRequest } from "@shared/proto/common"
import { McpServiceDefinition } from "@shared/proto/mcp"
// Generic type for any protobuf service definition
type ProtoService = {
name: string
@ -95,5 +95,6 @@ function createGrpcClient<T extends ProtoService>(service: T): GrpcClientType<T>
const BrowserServiceClient = createGrpcClient(BrowserServiceDefinition)
const CheckpointsServiceClient = createGrpcClient(CheckpointsServiceDefinition)
const McpServiceClient = createGrpcClient(McpServiceDefinition)
export { BrowserServiceClient, CheckpointsServiceClient }
export { BrowserServiceClient, CheckpointsServiceClient, McpServiceClient }