Skip to content

Commit 4189eb0

Browse files
author
Lasim
committed
feat(all): implement tool toggle functionality with optimistic updates
1 parent 7adc7b4 commit 4189eb0

File tree

7 files changed

+127
-15
lines changed

7 files changed

+127
-15
lines changed

services/backend/src/routes/teams/mcp-installations/toggle-tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export default async function toggleToolRoute(server: FastifyInstance) {
315315

316316
const action = is_disabled ? 'disabled' : 'enabled';
317317
const syncMessage = commandId
318-
? 'Change will sync to satellite within 2 seconds.'
318+
? 'Changes saved and sent to satellite.'
319319
: 'No active satellite available - change saved to database.';
320320

321321
const successResponse: SuccessResponse = {

services/frontend/src/components/mcp-server/installation/McpToolsTab.vue

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<script setup lang="ts">
22
import { ref, onMounted, computed } from 'vue'
33
import { useI18n } from 'vue-i18n'
4+
import { toast } from 'vue-sonner'
45
import { useMcpToolsStore } from '@/stores/mcpToolsStore'
6+
import { McpToolsService } from '@/services/mcpToolsService'
57
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
8+
import { Switch } from '@/components/ui/switch'
69
import { Alert, AlertDescription } from '@/components/ui/alert'
710
import { Chart } from '@/components/ui/chart'
811
import { AlertCircle, Package, Wrench, Coins } from 'lucide-vue-next'
@@ -19,16 +22,22 @@ use([TooltipComponent, LegendComponent, PieChart, CanvasRenderer])
1922
interface Props {
2023
installation: McpInstallation
2124
teamId: string
25+
canEdit?: boolean
26+
userRole?: 'team_admin' | 'team_user' | null
2227
}
2328
24-
const props = defineProps<Props>()
29+
const props = withDefaults(defineProps<Props>(), {
30+
canEdit: false,
31+
userRole: null
32+
})
2533
const { t } = useI18n()
2634
const mcpToolsStore = useMcpToolsStore()
2735
2836
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2937
const tools = ref<any>(null)
3038
const isLoading = ref(true)
3139
const error = ref<string | null>(null)
40+
const togglingToolId = ref<string | null>(null)
3241
3342
// Format token count with commas
3443
const formatTokenCount = (count: number) => {
@@ -58,6 +67,49 @@ const hasTools = computed(() => {
5867
return tools.value && tools.value.tools && tools.value.tools.length > 0
5968
})
6069
70+
// Handle tool toggle
71+
async function handleToolToggle(toolId: string, toolName: string, currentDisabled: boolean) {
72+
if (!props.canEdit) return
73+
74+
const newDisabledState = !currentDisabled
75+
togglingToolId.value = toolId
76+
77+
// Optimistic update
78+
const toolIndex = tools.value.tools.findIndex((t: { id: string }) => t.id === toolId)
79+
if (toolIndex !== -1) {
80+
tools.value.tools[toolIndex].is_disabled = newDisabledState
81+
}
82+
83+
try {
84+
const response = await McpToolsService.toggleToolStatus(
85+
props.teamId,
86+
props.installation.id,
87+
toolId,
88+
newDisabledState
89+
)
90+
91+
const action = newDisabledState
92+
? t('mcpInstallations.details.mcpTools.toggle.disabled')
93+
: t('mcpInstallations.details.mcpTools.toggle.enabled')
94+
95+
toast.success(t('mcpInstallations.details.mcpTools.toggle.success', { toolName, action }), {
96+
description: response.message
97+
})
98+
} catch (err) {
99+
// Revert optimistic update on error
100+
if (toolIndex !== -1) {
101+
tools.value.tools[toolIndex].is_disabled = currentDisabled
102+
}
103+
104+
const errorMessage = err instanceof Error ? err.message : t('mcpInstallations.details.mcpTools.toggle.error')
105+
toast.error(t('mcpInstallations.details.mcpTools.toggle.errorTitle'), {
106+
description: errorMessage
107+
})
108+
} finally {
109+
togglingToolId.value = null
110+
}
111+
}
112+
61113
// Pie chart configuration for token distribution
62114
const pieChartOption = computed<EChartsOption>(() => {
63115
if (!hasTools.value) return {}
@@ -167,13 +219,21 @@ const pieChartOption = computed<EChartsOption>(() => {
167219
<Table>
168220
<TableHeader>
169221
<TableRow>
222+
<TableHead class="w-20">{{ t('mcpInstallations.details.mcpTools.table.columns.enabled') }}</TableHead>
170223
<TableHead>{{ t('mcpInstallations.details.mcpTools.table.columns.toolName') }}</TableHead>
171224
<TableHead>{{ t('mcpInstallations.details.mcpTools.table.columns.description') }}</TableHead>
172225
<TableHead class="text-right">{{ t('mcpInstallations.details.mcpTools.table.columns.tokenCount') }}</TableHead>
173226
</TableRow>
174227
</TableHeader>
175228
<TableBody>
176229
<TableRow v-for="tool in tools.tools" :key="tool.id">
230+
<TableCell class="align-top">
231+
<Switch
232+
:model-value="!tool.is_disabled"
233+
:disabled="!props.canEdit || togglingToolId === tool.id"
234+
@update:model-value="handleToolToggle(tool.id, tool.tool_name, tool.is_disabled)"
235+
/>
236+
</TableCell>
177237
<TableCell class="text-sm font-medium align-top whitespace-nowrap">{{ tool.tool_name }}</TableCell>
178238
<TableCell class="text-sm text-muted-foreground max-w-2xl">
179239
<div class="whitespace-normal wrap-break-word">

services/frontend/src/components/ui/switch/Switch.vue

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,33 @@
11
<script setup lang="ts">
2-
import type { HTMLAttributes } from 'vue'
3-
import { reactiveOmit } from '@vueuse/core'
2+
import type { SwitchRootEmits, SwitchRootProps } from "reka-ui"
3+
import type { HTMLAttributes } from "vue"
4+
import { reactiveOmit } from "@vueuse/core"
45
import {
56
SwitchRoot,
6-
type SwitchRootEmits,
7-
type SwitchRootProps,
87
SwitchThumb,
98
useForwardPropsEmits,
10-
} from 'reka-ui'
11-
import { cn } from '@/lib/utils'
9+
} from "reka-ui"
10+
import { cn } from "@/lib/utils"
1211
13-
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes['class'] }>()
12+
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes["class"] }>()
1413
1514
const emits = defineEmits<SwitchRootEmits>()
1615
17-
const delegatedProps = reactiveOmit(props, 'class')
16+
const delegatedProps = reactiveOmit(props, "class")
1817
1918
const forwarded = useForwardPropsEmits(delegatedProps, emits)
2019
</script>
2120

2221
<template>
2322
<SwitchRoot
24-
data-slot="switch"
2523
v-bind="forwarded"
2624
:class="cn(
27-
'w-[42px] h-[25px] bg-gray-300 data-[state=checked]:bg-primary rounded-full relative cursor-default transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:cursor-not-allowed disabled:opacity-50',
25+
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
2826
props.class,
2927
)"
3028
>
3129
<SwitchThumb
32-
data-slot="switch-thumb"
33-
:class="cn('block w-[21px] h-[21px] bg-white rounded-full transition-transform duration-100 translate-x-0.5 data-[state=checked]:translate-x-[19px] shadow-sm')"
30+
:class="cn('pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5')"
3431
>
3532
<slot name="thumb" />
3633
</SwitchThumb>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { default as Switch } from './Switch.vue'
1+
export { default as Switch } from "./Switch.vue"

services/frontend/src/i18n/locales/en/mcp-installations.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@ export default {
624624
},
625625
table: {
626626
columns: {
627+
enabled: 'Enabled',
627628
toolName: 'Tool Name',
628629
description: 'Description',
629630
tokenCount: 'Token Count'
@@ -632,6 +633,13 @@ export default {
632633
noDescription: 'No description provided'
633634
}
634635
},
636+
toggle: {
637+
success: 'Tool "{toolName}" {action}',
638+
enabled: 'enabled',
639+
disabled: 'disabled',
640+
errorTitle: 'Failed to update tool',
641+
error: 'An error occurred while updating the tool status'
642+
},
635643
summary: {
636644
totalTools: 'Total Tools',
637645
totalTokens: 'Total Tokens',

services/frontend/src/services/mcpToolsService.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,19 @@ export interface McpTool {
1010
description: string
1111
input_schema: unknown
1212
token_count: number
13+
is_disabled: boolean
1314
discovered_at: string
1415
updated_at: string
1516
}
1617

18+
export interface ToggleToolResponse {
19+
tool_id: string
20+
tool_name: string
21+
is_disabled: boolean
22+
command_id?: string
23+
message: string
24+
}
25+
1726
export interface InstallationToolsResponse {
1827
installation_id: string
1928
installation_name: string
@@ -77,6 +86,42 @@ export class McpToolsService {
7786
return data.data || data
7887
}
7988

89+
/**
90+
* Toggle tool enabled/disabled status
91+
*/
92+
static async toggleToolStatus(
93+
teamId: string,
94+
installationId: string,
95+
toolId: string,
96+
isDisabled: boolean
97+
): Promise<ToggleToolResponse> {
98+
const response = await fetch(
99+
`${this.baseUrl}/api/teams/${teamId}/mcp/installations/${installationId}/tools/${toolId}`,
100+
{
101+
method: 'PATCH',
102+
credentials: 'include',
103+
headers: {
104+
'Content-Type': 'application/json',
105+
},
106+
body: JSON.stringify({ is_disabled: isDisabled }),
107+
}
108+
)
109+
110+
if (!response.ok) {
111+
const errorData = await response.json().catch(() => ({}))
112+
if (response.status === 403) {
113+
throw new Error('You don\'t have permission to manage tools')
114+
}
115+
if (response.status === 404) {
116+
throw new Error('Tool not found')
117+
}
118+
throw new Error(errorData.error || `Failed to update tool status: ${response.status}`)
119+
}
120+
121+
const data = await response.json()
122+
return data.data || data
123+
}
124+
80125
/**
81126
* Get team-wide tools summary (future phase)
82127
* Note: Backend endpoint not yet implemented

services/frontend/src/views/mcp-server/installation/[id].vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ const loadAndSetInstallation = async () => {
199199
v-if="activeTab === 'mcp-tools'"
200200
:installation="installation"
201201
:team-id="currentTeam.id"
202+
:can-edit="canEditInstallation"
203+
:user-role="userTeamRole"
202204
/>
203205
<TeamConfiguration
204206
v-if="activeTab === 'environment'"

0 commit comments

Comments
 (0)