Skip to content

Commit 6324982

Browse files
Add Remote Server Tab (RooCodeInc#2612)
* Add remote server form * changeset * naming
1 parent 70ab220 commit 6324982

File tree

6 files changed

+182
-12
lines changed

6 files changed

+182
-12
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Create UI for adding remote servers

src/core/controller/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,22 @@ export class Controller {
169169
case "addRemoteServer": {
170170
try {
171171
await this.mcpHub?.addRemoteServer(message.serverName!, message.serverUrl!)
172+
await this.postMessageToWebview({
173+
type: "addRemoteServerResult",
174+
addRemoteServerResult: {
175+
success: true,
176+
serverName: message.serverName!,
177+
},
178+
})
172179
} catch (error) {
173-
// We handle the errorin McpHub.ts where the function is defined
174-
console.error(`Failed to add remote server ${message.serverName}:`, error)
180+
await this.postMessageToWebview({
181+
type: "addRemoteServerResult",
182+
addRemoteServerResult: {
183+
success: false,
184+
serverName: message.serverName!,
185+
error: error.message,
186+
},
187+
})
175188
}
176189
break
177190
}

src/services/mcp/McpHub.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -659,24 +659,27 @@ export class McpHub {
659659
autoApprove: [],
660660
}
661661

662-
// TS expects the server config to be a McpServerConfig, but we know it's valid
663-
// The issue is that the type is not having the transportType field added to it
662+
const parsedConfig = ServerConfigSchema.parse(serverConfig)
664663

665-
// ToDo: Add input types reflecting the non-transformed version
666-
settings.mcpServers[serverName] = serverConfig as unknown as McpServerConfig
664+
settings.mcpServers[serverName] = parsedConfig
667665
const settingsPath = await this.getMcpSettingsFilePath()
668-
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2))
666+
667+
// We don't write the zod-transformed version to the file.
668+
// The above parse() call adds the transportType field to the server config
669+
// It would be fine if this was written, but we don't want to clutter up the file with internal details
670+
671+
// ToDo: We could benefit from input / output types reflecting the non-transformed / transformed versions
672+
await fs.writeFile(
673+
settingsPath,
674+
JSON.stringify({ mcpServers: { ...settings.mcpServers, [serverName]: serverConfig } }, null, 2),
675+
)
669676

670677
await this.updateServerConnections(settings.mcpServers)
671678

672-
vscode.window.showInformationMessage(`Added and connected to ${serverName} MCP server`)
679+
vscode.window.showInformationMessage(`Added ${serverName} MCP server`)
673680
} catch (error) {
674681
console.error("Failed to add remote MCP server:", error)
675682

676-
vscode.window.showErrorMessage(
677-
`Failed to add remote MCP server: ${error instanceof Error ? error.message : String(error)}`,
678-
)
679-
680683
throw error
681684
}
682685
}

src/shared/ExtensionMessage.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ExtensionMessage {
3535
| "openGraphData"
3636
| "isImageUrlResult"
3737
| "didUpdateSettings"
38+
| "addRemoteServerResult"
3839
| "userCreditsBalance"
3940
| "userCreditsUsage"
4041
| "userCreditsPayments"
@@ -80,6 +81,11 @@ export interface ExtensionMessage {
8081
userCreditsUsage?: UsageTransaction[]
8182
userCreditsPayments?: PaymentTransaction[]
8283
totalTasksSize?: number | null
84+
addRemoteServerResult?: {
85+
success: boolean
86+
serverName: string
87+
error?: string
88+
}
8389
}
8490

8591
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"

webview-ui/src/components/mcp/McpView.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import McpMarketplaceView from "./marketplace/McpMarketplaceView"
1818
import McpResourceRow from "./McpResourceRow"
1919
import McpToolRow from "./McpToolRow"
2020
import DangerButton from "../common/DangerButton"
21+
import AddRemoteServerForm from "./tabs/AddRemoteServerForm"
2122

2223
type McpViewProps = {
2324
onDone: () => void
@@ -81,6 +82,9 @@ const McpView = ({ onDone }: McpViewProps) => {
8182
Marketplace
8283
</TabButton>
8384
)}
85+
<TabButton isActive={activeTab === "addRemote"} onClick={() => handleTabChange("addRemote")}>
86+
Remote Servers
87+
</TabButton>
8488
<TabButton isActive={activeTab === "installed"} onClick={() => handleTabChange("installed")}>
8589
Installed
8690
</TabButton>
@@ -89,6 +93,7 @@ const McpView = ({ onDone }: McpViewProps) => {
8993
{/* Content container */}
9094
<div style={{ width: "100%" }}>
9195
{mcpMarketplaceEnabled && activeTab === "marketplace" && <McpMarketplaceView />}
96+
{activeTab === "addRemote" && <AddRemoteServerForm onServerAdded={() => handleTabChange("installed")} />}
9297
{activeTab === "installed" && (
9398
<div style={{ padding: "16px 20px" }}>
9499
<div
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { useCallback, useEffect, useRef, useState } from "react"
2+
import { vscode } from "../../../utils/vscode"
3+
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
4+
import { useEvent } from "react-use"
5+
6+
const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) => {
7+
const [serverName, setServerName] = useState("")
8+
const [serverUrl, setServerUrl] = useState("")
9+
const [isSubmitting, setIsSubmitting] = useState(false)
10+
const [error, setError] = useState("")
11+
const [showConnectingMessage, setShowConnectingMessage] = useState(false)
12+
13+
// Store submitted values to check if the server was added
14+
const submittedValues = useRef<{ name: string } | null>(null)
15+
16+
const handleMessage = useCallback(
17+
(event: MessageEvent) => {
18+
const message = event.data
19+
20+
if (
21+
message.type === "addRemoteServerResult" &&
22+
isSubmitting &&
23+
submittedValues.current &&
24+
message.addRemoteServerResult?.serverName === submittedValues.current.name
25+
) {
26+
if (message.addRemoteServerResult.success) {
27+
// Handle success
28+
setIsSubmitting(false)
29+
setServerName("")
30+
setServerUrl("")
31+
submittedValues.current = null
32+
onServerAdded()
33+
setShowConnectingMessage(false)
34+
} else {
35+
// Handle error
36+
setIsSubmitting(false)
37+
setError(message.addRemoteServerResult.error || "Failed to add server")
38+
setShowConnectingMessage(false)
39+
}
40+
}
41+
},
42+
[isSubmitting, onServerAdded],
43+
)
44+
45+
useEvent("message", handleMessage)
46+
47+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
48+
e.preventDefault()
49+
50+
if (!serverName.trim()) {
51+
setError("Server name is required")
52+
return
53+
}
54+
55+
if (!serverUrl.trim()) {
56+
setError("Server URL is required")
57+
return
58+
}
59+
60+
try {
61+
new URL(serverUrl)
62+
} catch (err) {
63+
setError("Invalid URL format")
64+
return
65+
}
66+
67+
setError("")
68+
69+
submittedValues.current = { name: serverName.trim() }
70+
71+
setIsSubmitting(true)
72+
setShowConnectingMessage(true)
73+
vscode.postMessage({
74+
type: "addRemoteServer",
75+
serverName: serverName.trim(),
76+
serverUrl: serverUrl.trim(),
77+
})
78+
}
79+
80+
return (
81+
<div className="p-4 px-5">
82+
<div className="text-[var(--vscode-foreground)] text-sm mb-4 mt-1 max-w-lg">
83+
Add a remote MCP server by providing a name and its URL endpoint.
84+
</div>
85+
86+
<form onSubmit={handleSubmit}>
87+
{error && (
88+
<div className="text-[var(--vscode-testing-iconFailed)] mb-3 p-2 rounded bg-[var(--vscode-inputValidation-warningBackground)] border border-[var(--vscode-inputValidation-warningBorder)]">
89+
{error}
90+
</div>
91+
)}
92+
93+
<div className="mb-4 mr-4">
94+
<label className="block mb-1">Server Name</label>
95+
<input
96+
type="text"
97+
value={serverName}
98+
onChange={(e) => {
99+
setServerName(e.target.value)
100+
setError("")
101+
}}
102+
disabled={isSubmitting}
103+
className="w-full max-w-md mr-4 p-2 bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border border-[var(--vscode-input-border)] rounded"
104+
/>
105+
</div>
106+
107+
<div className="mb-4 mr-4">
108+
<label className="block mb-1">Server URL</label>
109+
<input
110+
type="text"
111+
value={serverUrl}
112+
onChange={(e) => {
113+
setServerUrl(e.target.value)
114+
setError("")
115+
}}
116+
disabled={isSubmitting}
117+
placeholder="https://example.com/mcp-sse"
118+
className="w-full max-w-md mr-4 p-2 bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border border-[var(--vscode-input-border)] rounded"
119+
/>
120+
</div>
121+
122+
<div className="flex items-center mt-3">
123+
<VSCodeButton type="submit" disabled={isSubmitting}>
124+
{isSubmitting ? "Adding..." : "Add Server"}
125+
</VSCodeButton>
126+
127+
{showConnectingMessage && (
128+
<div className="ml-3 text-[var(--vscode-notificationsInfoIcon-foreground)] text-sm">
129+
Connecting to server... This may take a few seconds.
130+
</div>
131+
)}
132+
</div>
133+
</form>
134+
</div>
135+
)
136+
}
137+
138+
export default AddRemoteServerForm

0 commit comments

Comments
 (0)