feat: add mcp tab

This commit is contained in:
d-kimsuon
2025-09-03 20:03:47 +09:00
parent 7800037455
commit 155afeaf70
4 changed files with 168 additions and 3 deletions

View File

@@ -0,0 +1,96 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { RefreshCwIcon } from "lucide-react";
import type { FC } from "react";
import { Button } from "@/components/ui/button";
import { honoClient } from "@/lib/api/client";
export const McpTab: FC = () => {
const queryClient = useQueryClient();
const {
data: mcpData,
isLoading,
error,
} = useQuery({
queryKey: ["mcp", "list"],
queryFn: async () => {
const response = await honoClient.api.mcp.list.$get();
if (!response.ok) {
throw new Error("Failed to fetch MCP servers");
}
return response.json();
},
});
const handleReload = () => {
queryClient.invalidateQueries({ queryKey: ["mcp", "list"] });
};
return (
<div className="flex flex-col h-full">
<div className="p-3 border-b border-sidebar-border">
<div className="flex items-center justify-between">
<h2 className="text-sm font-semibold text-sidebar-foreground">
MCP Servers
</h2>
<Button
onClick={handleReload}
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
disabled={isLoading}
title="Reload MCP servers"
>
<RefreshCwIcon
className={`w-3 h-3 ${isLoading ? "animate-spin" : ""}`}
/>
</Button>
</div>
</div>
<div className="flex-1 overflow-auto p-3">
{isLoading && (
<div className="flex items-center justify-center h-32">
<div className="text-sm text-muted-foreground">Loading...</div>
</div>
)}
{error && (
<div className="text-sm text-red-500">
Failed to load MCP servers: {(error as Error).message}
</div>
)}
{mcpData && mcpData.servers.length === 0 && (
<div className="text-sm text-muted-foreground text-center py-8">
No MCP servers found
</div>
)}
{mcpData && mcpData.servers.length > 0 && (
<div className="space-y-3">
{mcpData.servers.map((server) => (
<div
key={server.name}
className="p-3 bg-sidebar-accent/50 rounded-md border border-sidebar-border"
>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<h3 className="text-sm font-medium text-sidebar-foreground truncate">
{server.name}
</h3>
<p className="text-xs text-muted-foreground mt-1 font-mono break-all">
{server.command}
</p>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};

View File

@@ -1,10 +1,11 @@
"use client";
import { MessageSquareIcon, SettingsIcon } from "lucide-react";
import { MessageSquareIcon, PlugIcon, SettingsIcon } from "lucide-react";
import { type FC, useState } from "react";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { useProject } from "../../../../hooks/useProject";
import { McpTab } from "./McpTab";
import { SessionsTab } from "./SessionsTab";
import { SettingsTab } from "./SettingsTab";
@@ -24,12 +25,12 @@ export const SessionSidebar: FC<{
const {
data: { sessions },
} = useProject(projectId);
const [activeTab, setActiveTab] = useState<"sessions" | "settings">(
const [activeTab, setActiveTab] = useState<"sessions" | "mcp" | "settings">(
"sessions",
);
const [isExpanded, setIsExpanded] = useState(true);
const handleTabClick = (tab: "sessions" | "settings") => {
const handleTabClick = (tab: "sessions" | "mcp" | "settings") => {
if (activeTab === tab && isExpanded) {
// If clicking the active tab while expanded, collapse
setIsExpanded(false);
@@ -50,6 +51,8 @@ export const SessionSidebar: FC<{
projectId={projectId}
/>
);
case "mcp":
return <McpTab />;
case "settings":
return <SettingsTab />;
default:
@@ -82,6 +85,21 @@ export const SessionSidebar: FC<{
<MessageSquareIcon className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => handleTabClick("mcp")}
className={cn(
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
activeTab === "mcp" && isExpanded
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
: "text-sidebar-foreground/70",
)}
title="MCP Servers"
>
<PlugIcon className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => handleTabClick("settings")}

View File

@@ -11,6 +11,7 @@ import type { SerializableAliveTask } from "../service/claude-code/types";
import { getEventBus } from "../service/events/EventBus";
import { getFileWatcher } from "../service/events/fileWatcher";
import { sseEventResponse } from "../service/events/sseEventResponse";
import { getMcpList } from "../service/mcp/getMcpList";
import { getProject } from "../service/project/getProject";
import { getProjects } from "../service/project/getProjects";
import { getSession } from "../service/session/getSession";
@@ -169,6 +170,11 @@ export const routes = (app: HonoAppType) => {
});
})
.get("/mcp/list", async (c) => {
const { servers } = await getMcpList();
return c.json({ servers });
})
.post(
"/projects/:projectId/new-session",
zValidator(

View File

@@ -0,0 +1,45 @@
import { execSync } from "node:child_process";
export interface McpServer {
name: string;
command: string;
}
export const getMcpList = async (): Promise<{ servers: McpServer[] }> => {
try {
const output = execSync("claude mcp list", {
encoding: "utf8",
timeout: 10000,
});
const servers: McpServer[] = [];
const lines = output.trim().split("\n");
for (const line of lines) {
// Skip header lines and status indicators
if (line.includes("Checking MCP server health") || line.trim() === "") {
continue;
}
// Parse lines like "context7: npx -y @upstash/context7-mcp@latest - ✓ Connected"
const colonIndex = line.indexOf(":");
if (colonIndex > 0) {
const name = line.substring(0, colonIndex).trim();
const rest = line.substring(colonIndex + 1).trim();
// Remove status indicators (✓ Connected, ✗ Failed, etc.)
const command = rest.replace(/\s*-\s*[✓✗].*$/, "").trim();
if (name && command) {
servers.push({ name, command });
}
}
}
return { servers };
} catch (error) {
console.error("Failed to get MCP list:", error);
// Return empty list if command fails
return { servers: [] };
}
};