Add McpHub and sync with McpView

This commit is contained in:
Saoud Rizwan
2024-12-05 19:00:55 -08:00
parent fa62548b01
commit 17d481d4d1
10 changed files with 577 additions and 215 deletions

View File

@@ -1,119 +1,75 @@
import {
VSCodeButton,
VSCodeDivider,
VSCodeTextArea,
VSCodeTextField,
VSCodeTag,
VSCodePanelTab,
VSCodePanelView,
VSCodeDataGrid,
VSCodeDataGridRow,
VSCodeDataGridCell,
VSCodePanels,
} from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
type McpServer = {
name: string
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
}
import { vscode } from "../../utils/vscode"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { McpServer } from "../../../../src/shared/mcp"
type McpViewProps = {
onDone: () => void
}
const McpView = ({ onDone }: McpViewProps) => {
const [isAdding, setIsAdding] = useState(false)
const [servers, setServers] = useState<McpServer[]>([
// Add some mock servers for testing
{
name: "local-tools",
config: JSON.stringify({
mcpServers: {
"local-tools": {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-tools"],
},
},
}),
status: "connected",
tools: [
{
name: "execute_command",
description: "Run a shell command on the local system",
},
{
name: "read_file",
description: "Read contents of a file from the filesystem",
},
],
},
{
name: "postgres-db",
config: JSON.stringify({
mcpServers: {
"postgres-db": {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
},
},
}),
status: "disconnected",
error: "Failed to connect to database: Connection refused",
},
{
name: "github-tools",
config: JSON.stringify({
mcpServers: {
"github-tools": {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
},
},
}),
status: "connecting",
resources: [
{
uri: "github://repo/issues",
name: "Repository Issues",
},
{
uri: "github://repo/pulls",
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)
}
}
const { mcpServers: servers } = useExtensionState()
// const [servers, setServers] = useState<McpServer[]>([
// // Add some mock servers for testing
// {
// name: "local-tools",
// config: JSON.stringify({
// mcpServers: {
// "local-tools": {
// command: "npx",
// args: ["-y", "@modelcontextprotocol/server-tools"],
// },
// },
// }),
// status: "connected",
// tools: [
// {
// name: "execute_command",
// description: "Run a shell command on the local system",
// },
// {
// name: "read_file",
// description: "Read contents of a file from the filesystem",
// },
// ],
// },
// {
// name: "postgres-db",
// config: JSON.stringify({
// mcpServers: {
// "postgres-db": {
// command: "npx",
// args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
// },
// },
// }),
// status: "disconnected",
// error: "Failed to connect to database: Connection refused",
// },
// {
// name: "github-tools",
// config: JSON.stringify({
// mcpServers: {
// "github-tools": {
// command: "npx",
// args: ["-y", "@modelcontextprotocol/server-github"],
// },
// },
// }),
// status: "connecting",
// resources: [
// {
// uri: "github://repo/issues",
// name: "Repository Issues",
// },
// {
// uri: "github://repo/pulls",
// name: "Pull Requests",
// },
// ],
// },
// ])
return (
<div
@@ -126,7 +82,6 @@ const McpView = ({ onDone }: McpViewProps) => {
display: "flex",
flexDirection: "column",
}}>
{/* Fixed Header */}
<div
style={{
display: "flex",
@@ -138,11 +93,10 @@ const McpView = ({ onDone }: McpViewProps) => {
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
{/* Scrollable Content */}
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
<p style={{ color: "var(--vscode-foreground)", fontSize: "13px" }}>
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.
</p>
@@ -151,51 +105,22 @@ const McpView = ({ onDone }: McpViewProps) => {
{servers.map((server) => (
<ServerRow key={server.name} server={server} />
))}
{/* Add Server UI as a row */}
{isAdding ? (
<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
style={{ flex: 1 }}
appearance="secondary"
onClick={() => setIsAdding(false)}>
Cancel
</VSCodeButton>
</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 */}
{/* Edit Settings Button */}
<div style={{ marginTop: "10px", width: "100%" }}>
<VSCodeButton
appearance="secondary"
style={{ width: "100%" }}
onClick={() => {
vscode.postMessage({ type: "openMcpSettings" })
}}>
<span className="codicon codicon-edit" style={{ marginRight: "6px" }}></span>
Edit MCP Settings
</VSCodeButton>
</div>
{/* Bottom padding */}
<div style={{ height: "20px" }} />
</div>
</div>
@@ -205,8 +130,6 @@ const McpView = ({ onDone }: McpViewProps) => {
// Server Row Component
const ServerRow = ({ server }: { server: McpServer }) => {
const [isExpanded, setIsExpanded] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [editConfig, setEditConfig] = useState(server.config)
const getStatusColor = () => {
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 = () => {
if (!server.error) {
setIsExpanded(!isExpanded)
}
}
const handleRetry = () => {
vscode.postMessage({
type: "retryMcpServer",
text: server.name,
})
}
return (
<div style={{ marginBottom: "10px" }}>
<div
@@ -271,18 +190,21 @@ const ServerRow = ({ server }: { server: McpServer }) => {
style={{
padding: "8px",
fontSize: "13px",
color: "var(--vscode-testing-iconFailed)",
background: "var(--vscode-list-hoverBackground)",
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>
) : (
isExpanded && (
<div
style={{
background: "var(--vscode-list-hoverBackground)",
padding: "0 12px 12px 12px",
padding: "0 12px 0 12px",
fontSize: "13px",
borderRadius: "0 0 4px 4px",
}}>
@@ -361,47 +283,6 @@ const ServerRow = ({ server }: { server: McpServer }) => {
)}
</VSCodePanelView>
</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>
)
)}

View File

@@ -10,12 +10,14 @@ import {
import { vscode } from "../utils/vscode"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { findLastIndex } from "../../../src/shared/array"
import { McpServer } from "../../../src/shared/mcp"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
theme: any
openRouterModels: Record<string, ModelInfo>
mcpServers: McpServer[]
filePaths: string[]
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
@@ -39,6 +41,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
@@ -95,6 +98,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
})
break
}
case "mcpServers": {
setMcpServers(message.mcpServers ?? [])
break
}
}
}, [])
@@ -110,6 +117,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
showWelcome,
theme,
openRouterModels,
mcpServers,
filePaths,
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),