Files
maestro/ui/src/components/userfolder/McpConnectionsPanel.tsx
T

130 lines
5.1 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
interface ConnectionRow {
serverId: string;
serverName: string;
connected: boolean;
authKind: 'oauth' | 'api_key';
ownerId: string | null;
}
interface ConnectionListResponse {
connections: ConnectionRow[];
}
async function fetchConnections(): Promise<ConnectionRow[]> {
const res = await fetch('/api/mcp/connections', { credentials: 'include' });
if (!res.ok) throw new Error(`${res.status}`);
const data: ConnectionListResponse = await res.json();
return data.connections ?? [];
}
async function disconnectMcp(serverId: string): Promise<void> {
const res = await fetch(`/api/mcp/connections/${encodeURIComponent(serverId)}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error(`${res.status}`);
}
function OwnerBadge({ ownerId }: { ownerId: string | null }) {
if (ownerId === null) {
return (
<span className="inline-block px-1.5 py-0.5 rounded text-[10px] font-medium bg-slate-100 text-slate-500 leading-none">
global
</span>
);
}
return (
<span className="inline-block px-1.5 py-0.5 rounded text-[10px] font-medium bg-blue-50 text-blue-600 leading-none">
personal
</span>
);
}
export function McpConnectionsPanel() {
const qc = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ['mcp-connections'],
queryFn: fetchConnections,
staleTime: 30_000,
});
const disconnect = useMutation({
mutationFn: disconnectMcp,
onSuccess: () => qc.invalidateQueries({ queryKey: ['mcp-connections'] }),
});
return (
<div className="h-full overflow-y-auto">
<div className="max-w-2xl mx-auto px-6 py-8">
<div className="mb-6">
<h2 className="text-base font-semibold text-slate-900 mb-1">MCP 接続</h2>
<p className="text-[13px] text-slate-500 leading-relaxed">
外部 MCP サーバーとの連携を管理します。OAuth サーバーは「連携する」を押すと
外部サービスの認可ページに飛び、戻ってくると自動で連携が確立します。
API key サーバーはサーバー登録と同時に接続済みになります。
</p>
</div>
{isLoading && <div className="text-[13px] text-slate-400">Loading</div>}
{error && <div className="text-[13px] text-red-500">読み込みに失敗しました: {String(error)}</div>}
{!isLoading && !error && (data?.length ?? 0) === 0 && (
<div className="text-[13px] text-slate-400">
利用可能な MCP サーバーがありません。「mcp-servers/」でサーバーを登録してください。
</div>
)}
<ul className="divide-y divide-hairline">
{(data ?? []).map((c) => (
<li key={c.serverId} className="py-3 flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span className="text-[13px] font-medium text-slate-900 truncate">{c.serverName}</span>
<OwnerBadge ownerId={c.ownerId} />
</div>
<div className="text-2xs text-slate-500 font-mono truncate">{c.serverId}</div>
</div>
<div className="shrink-0">
{c.authKind === 'oauth' ? (
c.connected ? (
<div className="flex items-center gap-3">
<span className="text-xs text-emerald-600 font-medium">連携済み</span>
<button
type="button"
className="text-xs text-slate-500 hover:text-slate-700 underline"
onClick={() => {
if (window.confirm(`${c.serverName} の連携を解除しますか?`)) {
disconnect.mutate(c.serverId);
}
}}
disabled={disconnect.isPending}
>解除</button>
</div>
) : (
<a
className="px-3 py-1 rounded-md text-xs font-semibold bg-accent text-accent-fg hover:bg-accent-deep transition-colors"
href={`/auth/mcp/${encodeURIComponent(c.serverId)}/start`}
>
連携する
</a>
)
) : (
/* api_key */
c.connected ? (
<div className="flex items-center gap-1.5">
<span className="text-xs text-emerald-600 font-medium">API key 接続済み</span>
{c.ownerId !== null && (
<span className="text-2xs text-slate-400">削除は mcp-servers タブから</span>
)}
</div>
) : (
<span className="text-xs text-amber-600">API key が未設定です</span>
)
)}
</div>
</li>
))}
</ul>
</div>
</div>
);
}