Skip to content

Commit dec68df

Browse files
committed
feat(frontend): add MCP Server management tab to team settings
Add a new "MCP Server" tab in team management that displays all MCP installations for a team. The new tab shows installation details including name, type, status, runtime, and timestamps in a table format with clickable rows that navigate to installation details. Changes: - Add getTeamMcpInstallations service method to fetch team MCP installations - Create new mcp-servers.vue view with table displaying MCP server installations - Add TeamMcpInstallation interface for type safety - Update TeamManageTabs component to include MCP Server navigation item - Add route configuration for /teams/manage/:id/mcp-servers
1 parent 0826af4 commit dec68df

File tree

4 files changed

+305
-0
lines changed

4 files changed

+305
-0
lines changed

services/frontend/src/components/teams/manage/TeamManageTabs.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ const router = useRouter()
1717
const menuItems = computed(() => [
1818
{ id: 'general', label: 'General', path: `/teams/manage/${props.teamId}/general` },
1919
{ id: 'members', label: 'Members', path: `/teams/manage/${props.teamId}/members` },
20+
{ id: 'mcp-servers', label: 'MCP Server', path: `/teams/manage/${props.teamId}/mcp-servers` },
2021
{ id: 'usage', label: 'Usage', path: `/teams/manage/${props.teamId}/usage` }
2122
])
2223
2324
// Map route names to section IDs
2425
const routeToSectionMap: Record<string, string> = {
2526
'TeamManageGeneral': 'general',
2627
'TeamManageMembers': 'members',
28+
'TeamManageMcpServers': 'mcp-servers',
2729
'TeamManageUsage': 'usage',
2830
}
2931

services/frontend/src/router/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,12 @@ const routes: RouteRecordRaw[] = [
273273
component: () => import('../views/teams/manage/[id]/members.vue'),
274274
meta: { requiresSetup: true },
275275
},
276+
{
277+
path: '/teams/manage/:id/mcp-servers',
278+
name: 'TeamManageMcpServers',
279+
component: () => import('../views/teams/manage/[id]/mcp-servers.vue'),
280+
meta: { requiresSetup: true },
281+
},
276282
{
277283
path: '/teams/manage/:id/usage',
278284
name: 'TeamManageUsage',

services/frontend/src/services/teamService.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ export interface TeamUsageLimits {
4747
non_http_mcp_limit: number;
4848
}
4949

50+
export interface TeamMcpInstallation {
51+
id: string
52+
installation_name: string
53+
installation_type: 'global' | 'team'
54+
team_id: string
55+
created_at: string
56+
last_used_at: string | null
57+
status: 'online' | 'offline' | 'error' | 'provisioning'
58+
status_message: string | null
59+
status_updated_at: string
60+
last_health_check_at: string | null
61+
server: {
62+
id: string
63+
icon_url: string | null
64+
category_id: string
65+
runtime: string
66+
}
67+
}
68+
5069
export interface TeamUsageData {
5170
is_default_team: boolean;
5271
total_installed_mcp_servers: number;
@@ -613,4 +632,45 @@ export class TeamService {
613632
throw error
614633
}
615634
}
635+
636+
/**
637+
* Get MCP installations for a team
638+
*/
639+
static async getTeamMcpInstallations(teamId: string): Promise<TeamMcpInstallation[]> {
640+
try {
641+
const apiUrl = this.getApiUrl()
642+
643+
const response = await fetch(`${apiUrl}/api/teams/${teamId}/mcp/installations`, {
644+
method: 'GET',
645+
headers: {
646+
'Content-Type': 'application/json',
647+
},
648+
credentials: 'include',
649+
})
650+
651+
if (!response.ok) {
652+
if (response.status === 401) {
653+
throw new Error('Unauthorized - please log in')
654+
}
655+
if (response.status === 403) {
656+
throw new Error('You do not have permission to view this team\'s MCP servers')
657+
}
658+
if (response.status === 404) {
659+
throw new Error('Team not found')
660+
}
661+
throw new Error(`Failed to fetch MCP servers: ${response.status}`)
662+
}
663+
664+
const data = await response.json()
665+
666+
if (data.success && Array.isArray(data.data)) {
667+
return data.data
668+
} else {
669+
throw new Error('Invalid response format')
670+
}
671+
} catch (error) {
672+
console.error('Error fetching team MCP installations:', error)
673+
throw error
674+
}
675+
}
616676
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted, onUnmounted, computed } from 'vue'
3+
import { useRouter } from 'vue-router'
4+
import { Skeleton } from '@/components/ui/skeleton'
5+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
6+
import { Badge } from '@/components/ui/badge'
7+
import { Alert, AlertDescription } from '@/components/ui/alert'
8+
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
9+
import { AlertCircle, Server } from 'lucide-vue-next'
10+
import NavbarLayout from '@/components/NavbarLayout.vue'
11+
import { TeamManageHeader, TeamManageTabs } from '@/components/teams/manage'
12+
import { useTeamCache } from '@/composables/teams/useTeamCache'
13+
import { useEventBus } from '@/composables/useEventBus'
14+
import { TeamService, type TeamMcpInstallation } from '@/services/teamService'
15+
16+
const router = useRouter()
17+
const eventBus = useEventBus()
18+
19+
const {
20+
team,
21+
isLoading: isLoadingTeam,
22+
error: teamError,
23+
teamId,
24+
loadAndSetTeam,
25+
initializeCache,
26+
setupWatchers,
27+
cleanupWatchers
28+
} = useTeamCache()
29+
30+
// MCP installations state
31+
const installations = ref<TeamMcpInstallation[]>([])
32+
const isLoadingInstallations = ref(true)
33+
const installationsError = ref<string | null>(null)
34+
35+
// Computed loading state
36+
const isLoading = computed(() => isLoadingTeam.value || isLoadingInstallations.value)
37+
const error = computed(() => teamError.value || installationsError.value)
38+
39+
// Handle team selection from sidebar
40+
const handleTeamSelected = (data: { teamId: string; teamName: string }) => {
41+
if (data.teamId !== teamId) {
42+
router.push(`/teams/manage/${data.teamId}/mcp-servers`)
43+
}
44+
}
45+
46+
// Load MCP installations
47+
async function loadInstallations() {
48+
isLoadingInstallations.value = true
49+
installationsError.value = null
50+
51+
try {
52+
installations.value = await TeamService.getTeamMcpInstallations(teamId)
53+
} catch (err) {
54+
installationsError.value = err instanceof Error ? err.message : 'Failed to load MCP servers'
55+
installations.value = []
56+
} finally {
57+
isLoadingInstallations.value = false
58+
}
59+
}
60+
61+
// Get status badge variant
62+
function getStatusBadgeVariant(status: string) {
63+
switch (status) {
64+
case 'online':
65+
return 'default'
66+
case 'offline':
67+
return 'secondary'
68+
case 'error':
69+
return 'destructive'
70+
case 'provisioning':
71+
return 'outline'
72+
default:
73+
return 'outline'
74+
}
75+
}
76+
77+
// Get status badge class
78+
function getStatusBadgeClass(status: string) {
79+
switch (status) {
80+
case 'online':
81+
return 'bg-green-50 text-green-700 border-green-200'
82+
case 'offline':
83+
return 'bg-neutral-50 text-neutral-700 border-neutral-200'
84+
case 'error':
85+
return ''
86+
case 'provisioning':
87+
return 'bg-yellow-50 text-yellow-700 border-yellow-200'
88+
default:
89+
return ''
90+
}
91+
}
92+
93+
// Format date
94+
function formatDate(dateString: string | null): string {
95+
if (!dateString) return 'Never'
96+
return new Date(dateString).toLocaleDateString()
97+
}
98+
99+
// Navigate to installation details
100+
function navigateToInstallation(installationId: string) {
101+
router.push(`/mcp-server/view/${installationId}`)
102+
}
103+
104+
// Load data on component mount
105+
onMounted(async () => {
106+
initializeCache()
107+
await loadAndSetTeam()
108+
setupWatchers()
109+
await loadInstallations()
110+
111+
// Listen for team selection events from sidebar
112+
eventBus.on('team-selected', handleTeamSelected)
113+
})
114+
115+
onUnmounted(() => {
116+
cleanupWatchers()
117+
eventBus.off('team-selected', handleTeamSelected)
118+
})
119+
</script>
120+
121+
<template>
122+
<NavbarLayout>
123+
<TeamManageHeader :team="team" :is-loading="isLoadingTeam" />
124+
125+
<div class="space-y-6 mt-6">
126+
<!-- Tabs - Always visible when team is loaded -->
127+
<TeamManageTabs v-if="team" :team="team" :team-id="teamId">
128+
<!-- Error State -->
129+
<Alert v-if="error" variant="destructive" class="mb-6">
130+
<AlertCircle class="h-4 w-4" />
131+
<AlertDescription>
132+
{{ error }}
133+
</AlertDescription>
134+
</Alert>
135+
136+
<!-- Loading State for Content -->
137+
<div v-else-if="isLoading" class="space-y-4">
138+
<Skeleton class="h-32 w-full rounded-lg" />
139+
<Skeleton class="h-32 w-full rounded-lg" />
140+
<Skeleton class="h-32 w-full rounded-lg" />
141+
</div>
142+
143+
<!-- Empty State -->
144+
<Empty v-else-if="installations.length === 0">
145+
<EmptyHeader>
146+
<EmptyMedia variant="icon">
147+
<Server />
148+
</EmptyMedia>
149+
<EmptyTitle>No MCP servers installed</EmptyTitle>
150+
<EmptyDescription>
151+
This team has not installed any MCP servers yet.
152+
</EmptyDescription>
153+
</EmptyHeader>
154+
</Empty>
155+
156+
<!-- MCP Installations Table -->
157+
<div v-else class="space-y-4">
158+
<div class="rounded-md border">
159+
<Table>
160+
<TableHeader>
161+
<TableRow>
162+
<TableHead>Name</TableHead>
163+
<TableHead>Type</TableHead>
164+
<TableHead>Status</TableHead>
165+
<TableHead>Runtime</TableHead>
166+
<TableHead>Created</TableHead>
167+
<TableHead>Last Used</TableHead>
168+
</TableRow>
169+
</TableHeader>
170+
<TableBody>
171+
<TableRow
172+
v-for="installation in installations"
173+
:key="installation.id"
174+
class="cursor-pointer hover:bg-muted/50"
175+
@click="navigateToInstallation(installation.id)"
176+
>
177+
<!-- Name with Icon -->
178+
<TableCell>
179+
<div class="flex items-center gap-2">
180+
<img
181+
v-if="installation.server.icon_url"
182+
:src="installation.server.icon_url"
183+
:alt="installation.installation_name"
184+
class="h-6 w-6 rounded"
185+
/>
186+
<Server v-else class="h-6 w-6 text-muted-foreground" />
187+
<span class="font-medium">{{ installation.installation_name }}</span>
188+
</div>
189+
</TableCell>
190+
191+
<!-- Type -->
192+
<TableCell>
193+
<Badge variant="outline" class="text-xs">
194+
{{ installation.installation_type }}
195+
</Badge>
196+
</TableCell>
197+
198+
<!-- Status -->
199+
<TableCell>
200+
<Badge
201+
:variant="getStatusBadgeVariant(installation.status)"
202+
:class="getStatusBadgeClass(installation.status)"
203+
>
204+
{{ installation.status }}
205+
</Badge>
206+
</TableCell>
207+
208+
<!-- Runtime -->
209+
<TableCell>
210+
<span class="text-sm text-muted-foreground">{{ installation.server.runtime }}</span>
211+
</TableCell>
212+
213+
<!-- Created -->
214+
<TableCell>
215+
<span class="text-sm">{{ formatDate(installation.created_at) }}</span>
216+
</TableCell>
217+
218+
<!-- Last Used -->
219+
<TableCell>
220+
<span class="text-sm text-muted-foreground">{{ formatDate(installation.last_used_at) }}</span>
221+
</TableCell>
222+
</TableRow>
223+
</TableBody>
224+
</Table>
225+
</div>
226+
227+
<!-- Results Counter -->
228+
<div class="flex items-center justify-between px-4 py-4">
229+
<div class="flex-1 text-sm text-muted-foreground">
230+
{{ installations.length }} {{ installations.length === 1 ? 'server' : 'servers' }} installed
231+
</div>
232+
</div>
233+
</div>
234+
</TeamManageTabs>
235+
</div>
236+
</NavbarLayout>
237+
</template>

0 commit comments

Comments
 (0)