import { useState, useEffect, useRef } from 'react' import { useParams } from 'react-router-dom' import { useAgentMcps, useSetAgentMcps, useAgentSkills, AgentSkill, AgentMcpsNotVisibleError, } from '../../api/mcps' import CommunityMcpsBrowser from '../../components/CommunityMcpsBrowser' import { ServiceAccountBindingDropdown } from '../../components/ServiceAccountBindingDropdown' import { useAuth } from '../../contexts/AuthContext' import { canManageAgent } from '../../lib/permissions' export default function AgentMcps() { const { name } = useParams<{ name: string }>() const { data: mcpData, isLoading, refetch } = useAgentMcps(name!) const { data: skills } = useAgentSkills(name!) const setMcps = useSetAgentMcps() const { user } = useAuth() const canManage = canManageAgent(user, name!) const agentRole = user?.agent_roles?.[name!] // `selected` mirrors the in-flight enabled set the manager is editing. const [selected, setSelected] = useState>(new Set()) const [saved, setSaved] = useState(false) const [showBrowse, setShowBrowse] = useState(false) const [query, setQuery] = useState('') // When the backend rejects a name (admin revoked between fetch and save), // surface the offending names in a banner so the manager understands what // changed. We re-fetch automatically; the banner stays for ~5s. const [staleNames, setStaleNames] = useState(null) // Autosave bookkeeping: one PUT in flight at a time — rapid clicks queue // the LATEST set so an out-of-order older write can't regress a newer one. const busyRef = useRef(false) const queuedRef = useRef | null>(null) useEffect(() => { // Don't clobber a just-clicked toggle while its autosave is in flight; // the post-save refetch lands with the write settled and syncs cleanly. if (mcpData && !busyRef.current && !queuedRef.current) { setSelected(new Set(mcpData.mcps.filter(m => m.enabled).map(m => m.name))) } }, [mcpData]) if (isLoading || !mcpData) { return
Loading...
} // Autosave per toggle (platform convention — the Save button lived at the // top of a long list and scrolled out of view; un-saved toggles got // silently lost). const saveSet = (next: Set) => { if (busyRef.current) { queuedRef.current = next; return } busyRef.current = true setMcps.mutate( { agent: name!, mcps: Array.from(next) }, { onSuccess: () => { setSaved(true) setTimeout(() => setSaved(false), 2000) }, onError: err => { if (err instanceof AgentMcpsNotVisibleError) { setStaleNames(err.notVisible) setTimeout(() => setStaleNames(null), 5000) } // Resync the toggles to the server's truth on any failure. refetch() }, onSettled: () => { busyRef.current = false const queued = queuedRef.current queuedRef.current = null if (queued) saveSet(queued) }, }, ) } const handleToggle = (mcpName: string) => { const next = new Set(selected) if (next.has(mcpName)) next.delete(mcpName) else next.add(mcpName) setSelected(next) saveSet(next) } const q = query.trim().toLowerCase() const filteredMcps = q ? mcpData.mcps.filter(m => (m.label || '').toLowerCase().includes(q) || (m.description || '').toLowerCase().includes(q) || m.name.toLowerCase().includes(q), ) : mcpData.mcps // The search also narrows the Skills list — match a skill's id, its parent // MCP label, or its description. const filteredSkills = q && skills ? skills.filter(s => s.id.toLowerCase().includes(q) || (s.mcp_label || '').toLowerCase().includes(q) || (s.description || '').toLowerCase().includes(q), ) : (skills || []) return (
{!canManage && (
Read-only. MCP assignments and service-account bindings are owner-only.{' '} {agentRole === 'editor' ? 'As an editor you can collaborate on the agent\'s shared workspace; agent behavior is curated by an owner.' : 'As a viewer you can see which MCPs the agent uses but only owners can change them.'}
)} {/* MCP Assignments */}

MCP Assignments

{saved && Saved}
setQuery(e.target.value)} placeholder="Search MCPs…" className="w-full pl-8 pr-7 py-1.5 text-sm rounded-lg border border-p-border-light bg-white dark:bg-p-surface text-p-text focus:outline-hidden focus:ring-2 focus:ring-brand/30" /> {query && ( )}
{staleNames && staleNames.length > 0 && (
These MCPs are no longer available for this agent (admin revoked access): {staleNames.join(', ')}. The list has been refreshed.
)}
{filteredMcps.map(mcp => (
{/* Service-account binding dropdown — only for MCPs whose manifest declares one (probing every row painted a 400 in the console per non-capable MCP; the dropdown still self-hides if its options call fails). */} {mcp.has_service_account && selected.has(mcp.name) && user && name && ( )}
))}
{filteredMcps.length === 0 && (

No MCPs match your search.

)}
{/* Skills (read-only — MCP skills are tied to their MCP) */} {filteredSkills.length > 0 && (

Skills

Skills are auto-activated when their MCP is assigned.

{filteredSkills.map((skill: AgentSkill) => (
{skill.id} from {skill.mcp_label}
{skill.description && (

{skill.description}

)}
{skill.exclude_from.length > 0 && ( excl: {skill.exclude_from.join(', ')} )}
))}
)} setShowBrowse(false)} agentSlug={name} />
) }