130 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|