Skip to content

Commit 6186e7b

Browse files
committed
feat(all): add MCP servers page to admin teams detail with pagination
Add a new MCP Servers sub-page to the admin teams detail section that displays team MCP server installations with pagination. Server names are clickable and link to the catalog detail page. Frontend changes: - Add new route /admin/teams/:id/mcp-server - Create TeamDetailMcpServers component with table and pagination - Update TeamDetailTabs navigation to include MCP Servers tab - Add i18n translations for MCP servers section - Implement white-light status badge design matching catalog page Backend changes: - Update MCP installations API response schema - Add server_id field to API response - Rename id to installation_id for clarity - Update database query to include server_id from join
1 parent 335715b commit 6186e7b

File tree

10 files changed

+366
-10
lines changed

10 files changed

+366
-10
lines changed

services/backend/api-spec.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35399,10 +35399,14 @@
3539935399
"items": {
3540035400
"type": "object",
3540135401
"properties": {
35402-
"id": {
35402+
"installation_id": {
3540335403
"type": "string",
3540435404
"description": "Installation unique identifier"
3540535405
},
35406+
"server_id": {
35407+
"type": "string",
35408+
"description": "MCP server unique identifier"
35409+
},
3540635410
"installation_name": {
3540735411
"type": "string",
3540835412
"description": "User-defined installation name"
@@ -35430,7 +35434,8 @@
3543035434
}
3543135435
},
3543235436
"required": [
35433-
"id",
35437+
"installation_id",
35438+
"server_id",
3543435439
"installation_name",
3543535440
"server_name",
3543635441
"server_slug",

services/backend/api-spec.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25026,9 +25026,12 @@ paths:
2502625026
items:
2502725027
type: object
2502825028
properties:
25029-
id:
25029+
installation_id:
2503025030
type: string
2503125031
description: Installation unique identifier
25032+
server_id:
25033+
type: string
25034+
description: MCP server unique identifier
2503225035
installation_name:
2503325036
type: string
2503425037
description: User-defined installation name
@@ -25049,7 +25052,8 @@ paths:
2504925052
nullable: true
2505025053
description: ISO8601 timestamp or null
2505125054
required:
25052-
- id
25055+
- installation_id
25056+
- server_id
2505325057
- installation_name
2505425058
- server_name
2505525059
- server_slug

services/backend/src/routes/admin/teams/mcp-installations.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ export default async function getTeamMcpInstallationsAdminRoute(server: FastifyI
8282

8383
const installations = await db
8484
.select({
85-
id: schema.mcpServerInstallations.id,
85+
installation_id: schema.mcpServerInstallations.id,
86+
server_id: schema.mcpServerInstallations.server_id,
8687
installation_name: schema.mcpServerInstallations.installation_name,
8788
server_name: schema.mcpServers.name,
8889
server_slug: schema.mcpServers.slug,
@@ -100,7 +101,8 @@ export default async function getTeamMcpInstallationsAdminRoute(server: FastifyI
100101

101102
// 4. Serialize installations
102103
const serializedInstallations = installations.map(inst => ({
103-
id: inst.id,
104+
installation_id: inst.installation_id,
105+
server_id: inst.server_id,
104106
installation_name: inst.installation_name,
105107
server_name: inst.server_name ?? 'Unknown Server',
106108
server_slug: inst.server_slug ?? 'unknown',

services/backend/src/routes/admin/teams/schemas.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,15 +229,16 @@ export function validatePaginationParams(query: PaginationQuery): { limit: numbe
229229
export const MCP_INSTALLATION_SCHEMA = {
230230
type: 'object',
231231
properties: {
232-
id: { type: 'string', description: 'Installation unique identifier' },
232+
installation_id: { type: 'string', description: 'Installation unique identifier' },
233+
server_id: { type: 'string', description: 'MCP server unique identifier' },
233234
installation_name: { type: 'string', description: 'User-defined installation name' },
234235
server_name: { type: 'string', description: 'MCP server name' },
235236
server_slug: { type: 'string', description: 'MCP server slug' },
236237
status: { type: 'string', description: 'Installation status (provisioning|online|offline|error|...)' },
237238
created_at: { type: 'string', description: 'ISO8601 timestamp' },
238239
last_used_at: { type: 'string', nullable: true, description: 'ISO8601 timestamp or null' }
239240
},
240-
required: ['id', 'installation_name', 'server_name', 'server_slug', 'status', 'created_at']
241+
required: ['installation_id', 'server_id', 'installation_name', 'server_name', 'server_slug', 'status', 'created_at']
241242
} as const;
242243

243244
export const MCP_INSTALLATIONS_RESPONSE_SCHEMA = {
@@ -261,7 +262,8 @@ export const MCP_INSTALLATIONS_RESPONSE_SCHEMA = {
261262

262263
// TypeScript interfaces for MCP installations
263264
export interface McpInstallation {
264-
id: string;
265+
installation_id: string;
266+
server_id: string;
265267
installation_name: string;
266268
server_name: string;
267269
server_slug: string;
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted } from 'vue'
3+
import { useI18n } from 'vue-i18n'
4+
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
5+
import { Skeleton } from '@/components/ui/skeleton'
6+
import {
7+
Table,
8+
TableBody,
9+
TableCell,
10+
TableHead,
11+
TableHeader,
12+
TableRow,
13+
} from '@/components/ui/table'
14+
import PaginationControls from '@/components/ui/pagination/PaginationControls.vue'
15+
import { Package, CircleCheck, CircleMinus, CircleAlert, Circle } from 'lucide-vue-next'
16+
import { getEnv } from '@/utils/env'
17+
18+
interface McpInstallation {
19+
installation_id: string
20+
server_id: string
21+
installation_name: string
22+
server_name: string
23+
server_slug: string
24+
status: string
25+
created_at: string
26+
last_used_at: string | null
27+
}
28+
29+
interface PaginationMetadata {
30+
total: number
31+
limit: number
32+
offset: number
33+
has_more: boolean
34+
}
35+
36+
interface McpInstallationsResponse {
37+
success: boolean
38+
data: {
39+
installations: McpInstallation[]
40+
pagination: PaginationMetadata
41+
}
42+
}
43+
44+
const props = defineProps<{
45+
teamId: string
46+
}>()
47+
48+
const { t } = useI18n()
49+
const apiUrl = getEnv('VITE_DEPLOYSTACK_BACKEND_URL') || ''
50+
51+
const installations = ref<McpInstallation[]>([])
52+
const isLoading = ref(true)
53+
const error = ref<string | null>(null)
54+
55+
// Pagination state
56+
const currentPage = ref(1)
57+
const pageSize = ref(20)
58+
const totalItems = ref(0)
59+
60+
// Fetch MCP installations from API
61+
async function fetchMcpInstallations(): Promise<McpInstallationsResponse> {
62+
if (!apiUrl) {
63+
throw new Error('VITE_DEPLOYSTACK_BACKEND_URL is not configured.')
64+
}
65+
66+
const offset = (currentPage.value - 1) * pageSize.value
67+
const url = `${apiUrl}/api/admin/teams/${props.teamId}/mcp/installations?limit=${pageSize.value}&offset=${offset}`
68+
69+
const response = await fetch(url, {
70+
credentials: 'include'
71+
})
72+
73+
if (!response.ok) {
74+
const errorData = await response.json().catch(() => ({}))
75+
throw new Error(errorData.error || `Failed to fetch MCP installations: ${response.statusText} (status: ${response.status})`)
76+
}
77+
78+
return await response.json()
79+
}
80+
81+
// Load installations
82+
async function loadInstallations() {
83+
try {
84+
isLoading.value = true
85+
const response = await fetchMcpInstallations()
86+
installations.value = response.data.installations
87+
totalItems.value = response.data.pagination.total
88+
error.value = null
89+
} catch (err) {
90+
error.value = err instanceof Error ? err.message : 'Failed to load MCP installations'
91+
installations.value = []
92+
totalItems.value = 0
93+
} finally {
94+
isLoading.value = false
95+
}
96+
}
97+
98+
// Pagination handlers
99+
const handlePageChange = async (page: number) => {
100+
currentPage.value = page
101+
await loadInstallations()
102+
}
103+
104+
const handlePageSizeChange = async (newPageSize: number) => {
105+
pageSize.value = newPageSize
106+
currentPage.value = 1
107+
await loadInstallations()
108+
}
109+
110+
// Get status icon component and classes
111+
const getStatusIcon = (status: string) => {
112+
switch (status) {
113+
case 'online':
114+
return {
115+
icon: CircleCheck,
116+
class: 'size-3 fill-green-500 text-green-500 dark:fill-green-400 dark:text-green-400'
117+
}
118+
case 'offline':
119+
return {
120+
icon: CircleMinus,
121+
class: 'size-3 text-muted-foreground'
122+
}
123+
case 'error':
124+
case 'permanently_failed':
125+
return {
126+
icon: CircleAlert,
127+
class: 'size-3 fill-red-500 text-red-500 dark:fill-red-400 dark:text-red-400'
128+
}
129+
case 'requires_reauth':
130+
return {
131+
icon: CircleAlert,
132+
class: 'size-3 fill-yellow-500 text-yellow-500 dark:fill-yellow-400 dark:text-yellow-400'
133+
}
134+
case 'provisioning':
135+
case 'connecting':
136+
case 'discovering_tools':
137+
case 'syncing_tools':
138+
case 'restarting':
139+
case 'command_received':
140+
return {
141+
icon: Circle,
142+
class: 'size-3 fill-blue-500 text-blue-500 dark:fill-blue-400 dark:text-blue-400'
143+
}
144+
default:
145+
return {
146+
icon: Circle,
147+
class: 'size-3 text-muted-foreground'
148+
}
149+
}
150+
}
151+
152+
// Format date for display
153+
const formatDate = (dateString: string) => {
154+
return new Date(dateString).toLocaleDateString()
155+
}
156+
157+
onMounted(async () => {
158+
await loadInstallations()
159+
})
160+
</script>
161+
162+
<template>
163+
<div class="space-y-6">
164+
<p class="text-sm text-muted-foreground">
165+
MCP server installations configured for this team.
166+
</p>
167+
168+
<!-- Error State -->
169+
<div v-if="error" class="text-red-500">
170+
{{ t('adminTeams.mcpServers.errorLoading', { error }) }}
171+
</div>
172+
173+
<!-- Loading State with Skeleton -->
174+
<div v-else-if="isLoading" class="rounded-md border">
175+
<Table>
176+
<TableHeader>
177+
<TableRow>
178+
<TableHead>{{ t('adminTeams.mcpServers.table.serverName') }}</TableHead>
179+
<TableHead>{{ t('adminTeams.mcpServers.table.status') }}</TableHead>
180+
<TableHead>{{ t('adminTeams.mcpServers.table.createdAt') }}</TableHead>
181+
<TableHead>{{ t('adminTeams.mcpServers.table.lastUsedAt') }}</TableHead>
182+
</TableRow>
183+
</TableHeader>
184+
<TableBody>
185+
<TableRow v-for="i in 5" :key="i">
186+
<TableCell><Skeleton class="h-4 w-24" /></TableCell>
187+
<TableCell><Skeleton class="h-5 w-16" /></TableCell>
188+
<TableCell><Skeleton class="h-4 w-20" /></TableCell>
189+
<TableCell><Skeleton class="h-4 w-20" /></TableCell>
190+
</TableRow>
191+
</TableBody>
192+
</Table>
193+
</div>
194+
195+
<!-- Empty State -->
196+
<Empty v-else-if="installations.length === 0">
197+
<EmptyHeader>
198+
<EmptyMedia variant="icon">
199+
<Package />
200+
</EmptyMedia>
201+
</EmptyHeader>
202+
<EmptyTitle>{{ t('adminTeams.mcpServers.noInstallations') }}</EmptyTitle>
203+
<EmptyDescription>
204+
This team has not installed any MCP servers yet.
205+
</EmptyDescription>
206+
</Empty>
207+
208+
<!-- Data Table -->
209+
<div v-else class="space-y-4">
210+
<div class="rounded-md border">
211+
<Table>
212+
<TableHeader>
213+
<TableRow>
214+
<TableHead>{{ t('adminTeams.mcpServers.table.serverName') }}</TableHead>
215+
<TableHead>{{ t('adminTeams.mcpServers.table.status') }}</TableHead>
216+
<TableHead>{{ t('adminTeams.mcpServers.table.createdAt') }}</TableHead>
217+
<TableHead>{{ t('adminTeams.mcpServers.table.lastUsedAt') }}</TableHead>
218+
</TableRow>
219+
</TableHeader>
220+
<TableBody>
221+
<TableRow
222+
v-for="installation in installations"
223+
:key="installation.installation_id"
224+
>
225+
<TableCell class="font-medium">
226+
<router-link
227+
:to="`/admin/mcp-server-catalog/view/${installation.server_id}`"
228+
class="text-primary hover:underline"
229+
>
230+
{{ installation.server_name }}
231+
</router-link>
232+
</TableCell>
233+
<TableCell>
234+
<div
235+
class="inline-flex items-center justify-center rounded-full border px-1.5 py-0.5 text-xs font-medium text-muted-foreground gap-1"
236+
>
237+
<component
238+
:is="getStatusIcon(installation.status).icon"
239+
:class="getStatusIcon(installation.status).class"
240+
/>
241+
<span>{{ installation.status }}</span>
242+
</div>
243+
</TableCell>
244+
<TableCell>{{ formatDate(installation.created_at) }}</TableCell>
245+
<TableCell>
246+
{{ installation.last_used_at ? formatDate(installation.last_used_at) : t('adminTeams.mcpServers.table.never') }}
247+
</TableCell>
248+
</TableRow>
249+
</TableBody>
250+
</Table>
251+
</div>
252+
253+
<!-- Pagination Controls -->
254+
<PaginationControls
255+
v-if="totalItems > 0"
256+
:current-page="currentPage"
257+
:page-size="pageSize"
258+
:total-items="totalItems"
259+
:is-loading="isLoading"
260+
@page-change="handlePageChange"
261+
@page-size-change="handlePageSizeChange"
262+
/>
263+
</div>
264+
</div>
265+
</template>

services/frontend/src/components/admin/teams/TeamDetailTabs.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ const router = useRouter()
1717
const menuItems = computed(() => [
1818
{ id: 'general', label: 'General', path: `/admin/teams/${props.teamId}/general` },
1919
{ id: 'limits', label: 'Limits', path: `/admin/teams/${props.teamId}/limits` },
20-
{ id: 'members', label: 'Members', path: `/admin/teams/${props.teamId}/members` }
20+
{ id: 'members', label: 'Members', path: `/admin/teams/${props.teamId}/members` },
21+
{ id: 'mcp-server', label: 'MCP Servers', path: `/admin/teams/${props.teamId}/mcp-server` }
2122
])
2223
2324
// Map route names to section IDs
2425
const routeToSectionMap: Record<string, string> = {
2526
'AdminTeamDetailGeneral': 'general',
2627
'AdminTeamDetailLimits': 'limits',
2728
'AdminTeamDetailMembers': 'members',
29+
'AdminTeamDetailMcpServer': 'mcp-server',
2830
}
2931
3032
// Get current section from route name

services/frontend/src/components/admin/teams/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { default as TeamDetailPageHeading } from './TeamDetailPageHeading.vue'
33
export { default as GeneralTab } from './TeamDetailGeneral.vue'
44
export { default as LimitsTab } from './TeamDetailLimits.vue'
55
export { default as MembersTab } from './TeamDetailMembers.vue'
6+
export { default as McpServersTab } from './TeamDetailMcpServers.vue'

0 commit comments

Comments
 (0)