Skip to content

Commit e73e4e2

Browse files
author
Lasim
committed
feat(frontend): implement client activity tracking and polling
1 parent 01a130e commit e73e4e2

File tree

4 files changed

+209
-36
lines changed

4 files changed

+209
-36
lines changed

services/frontend/src/components/mcp-server/McpClientConnectionsCard.vue

Lines changed: 105 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,128 @@
11
<script setup lang="ts">
2+
import { ref, onMounted, onUnmounted } from 'vue'
3+
import { useI18n } from 'vue-i18n'
24
import Card from '@/components/ui/card/Card.vue'
35
import { Item } from '@/components/ui/item'
6+
import { McpClientActivityService } from '@/services/mcpClientActivityService'
7+
import type { McpClientActivity } from '@/services/mcpClientActivityService'
48
5-
interface ClientConnection {
6-
id: string
7-
name: string
8-
type: string
9-
connectedAt: string
10-
status: 'active' | 'inactive'
9+
const { t } = useI18n()
10+
11+
const activities = ref<McpClientActivity[]>([])
12+
const isLoading = ref(false)
13+
const error = ref<string | null>(null)
14+
let pollingInterval: number | null = null
15+
16+
function getRelativeTime(isoString: string): string {
17+
const now = new Date()
18+
const activityDate = new Date(isoString)
19+
const diffMs = now.getTime() - activityDate.getTime()
20+
const diffMinutes = Math.floor(diffMs / 60000)
21+
22+
if (diffMinutes < 1) return t('mcpServer.clientConnections.activity.justNow')
23+
if (diffMinutes === 1) return t('mcpServer.clientConnections.activity.minuteAgo')
24+
if (diffMinutes < 60) return t('mcpServer.clientConnections.activity.minutesAgo', { count: diffMinutes })
25+
26+
const diffHours = Math.floor(diffMinutes / 60)
27+
if (diffHours === 1) return t('mcpServer.clientConnections.activity.hourAgo')
28+
if (diffHours < 24) return t('mcpServer.clientConnections.activity.hoursAgo', { count: diffHours })
29+
30+
const diffDays = Math.floor(diffHours / 24)
31+
if (diffDays === 1) return t('mcpServer.clientConnections.activity.dayAgo')
32+
return t('mcpServer.clientConnections.activity.daysAgo', { count: diffDays })
33+
}
34+
35+
async function fetchClientActivity() {
36+
try {
37+
isLoading.value = true
38+
error.value = null
39+
40+
const response = await McpClientActivityService.getMyClientActivity({
41+
limit: 20,
42+
active_within_minutes: 30
43+
})
44+
45+
activities.value = response.data.activities
46+
} catch (err) {
47+
error.value = err instanceof Error ? err.message : t('mcpServer.clientConnections.error.failed')
48+
console.error('Failed to fetch MCP client activity:', err)
49+
} finally {
50+
isLoading.value = false
51+
}
52+
}
53+
54+
function startPolling() {
55+
pollingInterval = window.setInterval(() => {
56+
fetchClientActivity()
57+
}, 30000)
1158
}
1259
13-
const dummyClients: ClientConnection[] = [
14-
{
15-
id: '1',
16-
name: 'VS Code',
17-
type: 'Desktop Client',
18-
connectedAt: '2 hours ago',
19-
status: 'active'
20-
},
21-
{
22-
id: '2',
23-
name: 'Cursor',
24-
type: 'Desktop Client',
25-
connectedAt: '5 minutes ago',
26-
status: 'active'
60+
function stopPolling() {
61+
if (pollingInterval !== null) {
62+
clearInterval(pollingInterval)
63+
pollingInterval = null
2764
}
28-
]
65+
}
66+
67+
onMounted(async () => {
68+
await fetchClientActivity()
69+
startPolling()
70+
})
71+
72+
onUnmounted(() => {
73+
stopPolling()
74+
})
2975
</script>
3076

3177
<template>
3278
<Card variant="gray">
3379
<div class="px-6">
34-
<h3 class="text-lg font-semibold mb-4">Client Connections</h3>
35-
<div class="space-y-3">
80+
<h3 class="text-lg font-semibold mb-4">
81+
{{ t('mcpServer.clientConnections.title') }}
82+
</h3>
83+
84+
<div v-if="isLoading && activities.length === 0" class="text-center py-8 text-muted-foreground">
85+
{{ t('mcpServer.clientConnections.loading') }}
86+
</div>
87+
88+
<div v-else-if="error" class="text-center py-8">
89+
<p class="text-sm text-destructive mb-2">{{ error }}</p>
90+
<button
91+
@click="fetchClientActivity"
92+
class="text-sm text-primary hover:underline"
93+
>
94+
{{ t('mcpServer.clientConnections.error.retry') }}
95+
</button>
96+
</div>
97+
98+
<div v-else-if="activities.length === 0" class="text-center py-8 text-muted-foreground">
99+
<p class="mb-2">{{ t('mcpServer.clientConnections.empty.title') }}</p>
100+
<p class="text-xs">{{ t('mcpServer.clientConnections.empty.description') }}</p>
101+
</div>
102+
103+
<div v-else class="space-y-3">
36104
<Item
37-
v-for="client in dummyClients"
38-
:key="client.id"
105+
v-for="activity in activities"
106+
:key="activity.id"
39107
variant="filled"
40108
class="cursor-pointer"
41109
>
42110
<div class="flex items-center justify-between w-full">
43111
<div class="flex flex-col">
44-
<span class="font-medium">{{ client.name }}</span>
45-
<span class="text-sm text-muted-foreground">{{ client.type }}</span>
112+
<span class="font-medium">
113+
{{ activity.client_name || t('mcpServer.clientConnections.client.unknown') }}
114+
</span>
115+
<span class="text-sm text-muted-foreground">{{ activity.satellite.name }}</span>
46116
</div>
47117
<div class="flex flex-col items-end">
48-
<span class="text-xs text-muted-foreground">{{ client.connectedAt }}</span>
49-
<span
50-
class="text-xs font-medium"
51-
:class="{
52-
'text-green-600': client.status === 'active',
53-
'text-gray-400': client.status === 'inactive'
54-
}"
55-
>
56-
{{ client.status }}
118+
<span class="text-xs text-muted-foreground">
119+
{{ getRelativeTime(activity.last_activity_at) }}
120+
</span>
121+
<span class="text-xs text-green-600 font-medium">
122+
{{ t('mcpServer.clientConnections.activity.stats', {
123+
requests: activity.total_requests,
124+
tools: activity.total_tool_calls
125+
}) }}
57126
</span>
58127
</div>
59128
</div>

services/frontend/src/i18n/locales/en/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import credentialsMessages from './credentials'
1616
import mcpCatalogMessages from './mcp-catalog'
1717
import mcpCategoriesMessages from './mcp-categories'
1818
import mcpInstallationsMessages from './mcp-installations'
19+
import mcpServerMessages from './mcp-server'
1920
import oauthMessages from './oauth'
2021
import registerMessages from './register'
2122
import loginMessages from './login'
@@ -43,6 +44,7 @@ export default {
4344
...mcpCategoriesMessages,
4445
mcpCatalog: mcpCatalogMessages,
4546
mcpInstallations: mcpInstallationsMessages,
47+
mcpServer: mcpServerMessages,
4648
oauth: oauthMessages,
4749
register: registerMessages,
4850
login: loginMessages,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// @/i18n/locales/en/mcp-server.ts
2+
export default {
3+
clientConnections: {
4+
title: 'Client Connections',
5+
loading: 'Loading client connections...',
6+
empty: {
7+
title: 'No active MCP clients',
8+
description: 'Connect a client to see it here'
9+
},
10+
error: {
11+
failed: 'Failed to load client connections',
12+
retry: 'Try again'
13+
},
14+
activity: {
15+
justNow: 'just now',
16+
minuteAgo: '1 minute ago',
17+
minutesAgo: '{count} minutes ago',
18+
hourAgo: '1 hour ago',
19+
hoursAgo: '{count} hours ago',
20+
dayAgo: '1 day ago',
21+
daysAgo: '{count} days ago',
22+
requests: '{count} requests',
23+
tools: '{count} tools',
24+
stats: '{requests} requests · {tools} tools'
25+
},
26+
client: {
27+
unknown: 'Unknown Client'
28+
}
29+
}
30+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { getEnv } from '@/utils/env'
2+
3+
export interface McpClientActivity {
4+
id: string
5+
client_name: string
6+
satellite: {
7+
id: string
8+
name: string
9+
}
10+
last_activity_at: string
11+
total_requests: number
12+
total_tool_calls: number
13+
user_agent: string
14+
first_seen_at: string
15+
}
16+
17+
export interface McpClientActivityResponse {
18+
success: boolean
19+
data: {
20+
activities: McpClientActivity[]
21+
pagination: {
22+
total: number
23+
limit: number
24+
offset: number
25+
has_more: boolean
26+
}
27+
}
28+
}
29+
30+
export interface GetClientActivityParams {
31+
limit?: number
32+
offset?: number
33+
active_within_minutes?: number
34+
}
35+
36+
export class McpClientActivityService {
37+
private static baseUrl = getEnv('VITE_DEPLOYSTACK_BACKEND_URL')
38+
39+
/**
40+
* Get current user's active MCP client connections
41+
*/
42+
static async getMyClientActivity(
43+
params: GetClientActivityParams = {}
44+
): Promise<McpClientActivityResponse> {
45+
const queryParams = new URLSearchParams()
46+
47+
if (params.limit) queryParams.append('limit', params.limit.toString())
48+
if (params.offset) queryParams.append('offset', params.offset.toString())
49+
if (params.active_within_minutes) {
50+
queryParams.append('active_within_minutes', params.active_within_minutes.toString())
51+
}
52+
53+
const url = `${this.baseUrl}/api/users/me/mcp/client-activity${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
54+
55+
const response = await fetch(url, {
56+
method: 'GET',
57+
credentials: 'include',
58+
headers: {
59+
'Content-Type': 'application/json',
60+
},
61+
})
62+
63+
if (!response.ok) {
64+
const errorData = await response.json().catch(() => ({}))
65+
throw new Error(
66+
errorData.message || `Failed to fetch MCP client activity: ${response.status}`
67+
)
68+
}
69+
70+
return response.json()
71+
}
72+
}

0 commit comments

Comments
 (0)