feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user