Compare commits

...

1 Commits

5 changed files with 129 additions and 19 deletions
+4
View File
@@ -40,6 +40,10 @@ message AddRemoteMcpServerRequest {
Metadata metadata = 1;
string server_name = 2;
string server_url = 3;
// New fields for first-class transport setup
string transport_type = 4; // "streamableHttp" | "sse" | "stdio" (clients may default to "streamableHttp")
map<string, string> headers = 5; // optional headers (e.g., Authorization)
int32 timeout = 6; // optional per-server timeout in seconds
}
message ToggleToolAutoApproveRequest {
+13 -2
View File
@@ -19,8 +19,19 @@ export async function addRemoteMcpServer(controller: Controller, request: AddRem
throw new Error("Server URL is required")
}
// Call the McpHub method to add the remote server
const servers = await controller.mcpHub?.addRemoteServer(request.serverName, request.serverUrl)
// Optional inputs for first-class transport setup
const transportType = request.transportType || undefined
const headers = request.headers && Object.keys(request.headers).length > 0 ? request.headers : undefined
const timeout = typeof request.timeout === "number" && request.timeout > 0 ? request.timeout : undefined
// Call the McpHub method to add the remote server with transport details
const servers = await controller.mcpHub?.addRemoteServer(
request.serverName,
request.serverUrl,
transportType,
headers,
timeout,
)
const protoServers = convertMcpServersToProtoMcpServers(servers)
+53 -14
View File
@@ -312,6 +312,13 @@ export class McpHub {
}
await this.notifyWebviewOfServerChanges()
}
transport.onclose = async () => {
const connection = this.findConnection(name, source)
if (connection) {
connection.server.status = "disconnected"
}
await this.notifyWebviewOfServerChanges()
}
break
}
default:
@@ -510,6 +517,14 @@ export class McpHub {
try {
// Only close transport and client if they exist (disabled servers don't have them)
if (connection.transport) {
// Gracefully terminate Streamable HTTP session if applicable
try {
if (connection.transport instanceof StreamableHTTPClientTransport) {
await (connection.transport as StreamableHTTPClientTransport).terminateSession()
}
} catch (err) {
console.error(`Failed to terminate streamable HTTP session for ${name}:`, err)
}
await connection.transport.close()
}
if (connection.client) {
@@ -987,7 +1002,13 @@ export class McpHub {
}
}
public async addRemoteServer(serverName: string, serverUrl: string): Promise<McpServer[]> {
public async addRemoteServer(
serverName: string,
serverUrl: string,
transportType?: string,
headers?: Record<string, string>,
timeout?: number,
): Promise<McpServer[]> {
try {
const settings = await this.readAndValidateMcpSettingsFile()
if (!settings) {
@@ -1003,27 +1024,45 @@ export class McpHub {
throw new Error(`Invalid server URL: ${serverUrl}. Please provide a valid URL.`)
}
const serverConfig = {
url: serverUrl,
// Determine transport type (default to streamableHttp)
const type =
transportType === "sse" || transportType === "stdio" || transportType === "streamableHttp"
? (transportType as "sse" | "stdio" | "streamableHttp")
: ("streamableHttp" as const)
// Build server config to persist (include type explicitly)
const serverConfig: any = {
url: serverUrl, // TODO remove after answering question: dont we need the URL? as it required to know where to connect to? currently all the servers being used are STDIO so that should likely be the default here
type,
disabled: false,
autoApprove: [],
}
if (type === "stdio") {
throw new Error(
"STDIO servers cannot be added via the remote add form. Edit settings to configure local stdio servers.",
)
} else {
serverConfig.url = serverUrl
if (headers && Object.keys(headers).length > 0) {
serverConfig.headers = headers
}
}
if (typeof timeout === "number") {
serverConfig.timeout = timeout
}
// Validate against schema (also maps legacy transportType internally)
const parsedConfig = ServerConfigSchema.parse(serverConfig)
settings.mcpServers[serverName] = parsedConfig
// Persist exactly what the user intended (including 'type')
const settingsPath = await this.getMcpSettingsFilePath()
const updated = { ...settings.mcpServers, [serverName]: serverConfig }
await fs.writeFile(settingsPath, JSON.stringify({ mcpServers: updated }, null, 2))
// We don't write the zod-transformed version to the file.
// The above parse() call adds the transportType field to the server config
// It would be fine if this was written, but we don't want to clutter up the file with internal details
// ToDo: We could benefit from input / output types reflecting the non-transformed / transformed versions
await fs.writeFile(
settingsPath,
JSON.stringify({ mcpServers: { ...settings.mcpServers, [serverName]: serverConfig } }, null, 2),
)
// Use in-memory parsed config for live connections
settings.mcpServers[serverName] = parsedConfig
await this.updateServerConnectionsRPC(settings.mcpServers)
const serverOrder = Object.keys(settings.mcpServers || {})
+1 -1
View File
@@ -14,6 +14,6 @@ export type McpConnection = {
transport: Transport
}
export type McpTransportType = "stdio" | "sse" | "http"
export type McpTransportType = "stdio" | "sse" | "streamableHttp"
export type McpServerConfig = z.infer<typeof ServerConfigSchema>
@@ -1,7 +1,7 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { AddRemoteMcpServerRequest, McpServers } from "@shared/proto/cline/mcp"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { LINKS } from "@/constants"
import { useExtensionState } from "@/context/ExtensionStateContext"
@@ -10,6 +10,9 @@ import { McpServiceClient } from "@/services/grpc-client"
const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) => {
const [serverName, setServerName] = useState("")
const [serverUrl, setServerUrl] = useState("")
const [transportType, setTransportType] = useState<"streamableHttp" | "sse">("streamableHttp")
const [authHeader, setAuthHeader] = useState("")
const [timeoutSeconds, setTimeoutSeconds] = useState<string>("")
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState("")
const [showConnectingMessage, setShowConnectingMessage] = useState(false)
@@ -40,10 +43,16 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
setShowConnectingMessage(true)
try {
const headers = authHeader.trim() ? { Authorization: authHeader.trim() } : undefined
const timeout = timeoutSeconds.trim() ? Number(timeoutSeconds.trim()) : undefined
const servers: McpServers = await McpServiceClient.addRemoteMcpServer(
AddRemoteMcpServerRequest.create({
serverName: serverName.trim(),
serverUrl: serverUrl.trim(),
transportType,
headers,
timeout,
}),
)
@@ -54,6 +63,9 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
setServerName("")
setServerUrl("")
setAuthHeader("")
setTimeoutSeconds("")
setTransportType("streamableHttp")
onServerAdded()
setShowConnectingMessage(false)
} catch (error) {
@@ -66,7 +78,7 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
return (
<div className="p-4 px-5">
<div className="text-[var(--vscode-foreground)] mb-2">
Add a remote MCP server by providing a name and its URL endpoint. Learn more{" "}
Add a remote MCP server by providing a name, transport, and URL endpoint. Learn more{" "}
<VSCodeLink href={LINKS.DOCUMENTATION.REMOTE_MCP_SERVER_DOCS} style={{ display: "inline" }}>
here.
</VSCodeLink>
@@ -87,6 +99,22 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
</VSCodeTextField>
</div>
<div className="mb-2">
<label className="block mb-1 text-[var(--vscode-foreground)]">Transport</label>
<VSCodeDropdown
className="w-full"
disabled={isSubmitting}
onChange={(e) => {
const val = (e.target as HTMLSelectElement).value as "streamableHttp" | "sse"
setTransportType(val)
setError("")
}}
value={transportType}>
<VSCodeOption value="streamableHttp">Streamable HTTP (recommended)</VSCodeOption>
<VSCodeOption value="sse">SSE</VSCodeOption>
</VSCodeDropdown>
</div>
<div className="mb-2">
<VSCodeTextField
className="w-full mr-4"
@@ -101,6 +129,34 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
</VSCodeTextField>
</div>
<div className="mb-2">
<VSCodeTextField
className="w-full"
disabled={isSubmitting}
onChange={(e) => {
setAuthHeader((e.target as HTMLInputElement).value)
setError("")
}}
placeholder="Bearer <token>"
value={authHeader}>
Authorization Header (optional)
</VSCodeTextField>
</div>
<div className="mb-2">
<VSCodeTextField
className="w-full"
disabled={isSubmitting}
onChange={(e) => {
setTimeoutSeconds((e.target as HTMLInputElement).value)
setError("")
}}
placeholder="60"
value={timeoutSeconds}>
Timeout (seconds, optional)
</VSCodeTextField>
</div>
{error && <div className="mb-3 text-[var(--vscode-errorForeground)]">{error}</div>}
<div className="flex items-center mt-3 w-full">