Skip to content

Commit 8af656c

Browse files
DumbrisAlgis Dumbris
andauthored
feat(frontend): add per-server Auto-approve tool changes toggle (MCP-2932) (#725)
Add an 'Auto-approve tool changes' toggle to the server Configuration tab, bound to the per-server auto_approve_tool_changes flag (MCP-2930). OFF by default (protected); a rug-pull warning hint sits directly beneath it. Toggling persists through the existing PATCH /api/v1/servers/{id} path and surfaces a success toast. - types/api.ts: add optional auto_approve_tool_changes to Server (undefined = OFF) - ServerDetail.vue: Tool-change approval card with toggle + warning + handler - docs(tool-quarantine): document the Web UI toggle - test: component test covering reflect/persist + warning render Related #MCP-2916 Co-authored-by: Algis Dumbris <gordon.greatests@gmail.com>
1 parent e8d98a7 commit 8af656c

4 files changed

Lines changed: 198 additions & 0 deletions

File tree

docs/features/tool-quarantine.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,16 @@ appear at once.
272272

273273
The server list page shows a quarantine badge with the count of pending/changed tools for each server.
274274

275+
The server detail view's **Configuration** tab carries an **Auto-approve tool
276+
changes** toggle (a **Tool-change approval** card) bound to the per-server
277+
`auto_approve_tool_changes` flag. It is **OFF by default (protected)**; a ⚠️ hint
278+
beneath it explains that enabling it disables rug-pull protection for that
279+
server — future tool description/schema changes and newly added tools are then
280+
trusted automatically instead of held for review. Toggling persists through the
281+
existing `PATCH /api/v1/servers/{id}` path. (As noted under
282+
[Per-Server Auto-Approve](#per-server-auto-approve), the runtime enforcement of
283+
this flag rolls out in an upcoming release.)
284+
275285
### Doctor Command
276286

277287
The `mcpproxy doctor` command includes a "Tools Pending Quarantine Approval" section:

frontend/src/types/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ export interface Server {
160160
protocol: 'http' | 'stdio' | 'streamable-http'
161161
enabled: boolean
162162
quarantined: boolean
163+
// Per-server intent to auto-approve tool changes/additions (MCP-2930).
164+
// When true, rug-pull protection is disabled for this server: changed and
165+
// newly-added tools are trusted automatically instead of held for review.
166+
// Optional because the REST status payload only includes it once the
167+
// backend exposes the flag; absent/undefined is treated as OFF (protected).
168+
auto_approve_tool_changes?: boolean
163169
connected: boolean
164170
connecting: boolean
165171
authenticated?: boolean

frontend/src/views/ServerDetail.vue

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,60 @@
705705
</div>
706706
</div>
707707

708+
<!-- Tool-change approval (rug-pull protection) — MCP-2932.
709+
Bound to the per-server `auto_approve_tool_changes` config flag
710+
(MCP-2930). OFF by default = protected: a tool whose
711+
description/schema changes, or a newly-added tool, is held for
712+
review before AI agents can use it. ON trusts those changes
713+
automatically, disabling rug-pull protection for this server. -->
714+
<div class="card bg-base-100 shadow-sm" data-test="auto-approve-card">
715+
<div class="card-body py-4">
716+
<h3 class="card-title text-base">Tool-change approval</h3>
717+
<label class="flex items-center gap-3 mt-2 cursor-pointer">
718+
<input
719+
type="checkbox"
720+
data-test="auto-approve-tool-changes"
721+
:checked="autoApproveToolChanges"
722+
@change="toggleAutoApproveToolChanges"
723+
class="toggle toggle-sm toggle-warning"
724+
:disabled="kvPatchInFlight"
725+
/>
726+
<span class="text-sm font-medium">Auto-approve tool changes</span>
727+
</label>
728+
<!-- Rug-pull warning sits directly beneath the toggle. Always
729+
visible so the trade-off is clear before enabling; it
730+
escalates to an alert once the protection is actually off. -->
731+
<div
732+
v-if="autoApproveToolChanges"
733+
data-test="auto-approve-warning"
734+
role="alert"
735+
class="alert alert-warning mt-2 py-2 text-sm"
736+
>
737+
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
738+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
739+
</svg>
740+
<span>
741+
Rug-pull protection is <strong>disabled</strong> for this server.
742+
Future changes to a tool's description or schema — and newly
743+
added tools — are trusted automatically instead of held for review.
744+
</span>
745+
</div>
746+
<p
747+
v-else
748+
data-test="auto-approve-warning"
749+
class="text-xs text-base-content/60 mt-2 flex items-start gap-1.5"
750+
>
751+
<span aria-hidden="true">⚠️</span>
752+
<span>
753+
Enabling this <strong>disables rug-pull protection</strong>: changed
754+
tool descriptions/schemas and newly added tools will be trusted
755+
automatically instead of held for review. Protected (default) is
756+
recommended.
757+
</span>
758+
</p>
759+
</div>
760+
</div>
761+
708762
<!-- Connection (HTTP/SSE) -->
709763
<div v-if="server.url" class="card bg-base-100 shadow-sm">
710764
<div class="card-body py-4">
@@ -2617,6 +2671,27 @@ function scopeKey(scope: 'header' | 'env'): 'headers' | 'env' {
26172671
return scope === 'header' ? 'headers' : 'env'
26182672
}
26192673
2674+
// MCP-2932: per-server "Auto-approve tool changes" toggle. Absent/undefined on
2675+
// the status payload is treated as OFF (protected) — see the Server type note.
2676+
const autoApproveToolChanges = computed(() => server.value?.auto_approve_tool_changes ?? false)
2677+
2678+
async function toggleAutoApproveToolChanges(event: Event) {
2679+
const checked = (event.target as HTMLInputElement).checked
2680+
// Persist through the existing PATCH /api/v1/servers/{id} path. The backend
2681+
// auto-approves changed/added tools on the next discovery pass for this
2682+
// server (MCP-2931); patchServerDiff surfaces the success toast.
2683+
const ok = await patchServerDiff(
2684+
{ auto_approve_tool_changes: checked },
2685+
checked ? 'Auto-approve tool changes enabled' : 'Auto-approve tool changes disabled'
2686+
)
2687+
// On failure, snap the checkbox back to the persisted value: patchServerDiff
2688+
// refetches servers on success, so the bound computed already reflects truth;
2689+
// an explicit no-op here keeps the control consistent with `server`.
2690+
if (!ok && event.target) {
2691+
;(event.target as HTMLInputElement).checked = autoApproveToolChanges.value
2692+
}
2693+
}
2694+
26202695
async function saveEdit(scope: 'header' | 'env', k: string, val: string) {
26212696
const ok = await patchServerDiff({ [scopeKey(scope)]: { [k]: val } }, `Updated ${k}`)
26222697
if (ok) editingKey.value = null
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, it, expect, beforeEach, vi } from 'vitest'
2+
import { mount, flushPromises } from '@vue/test-utils'
3+
import { createPinia, setActivePinia } from 'pinia'
4+
import { createRouter, createWebHistory } from 'vue-router'
5+
6+
// MCP-2932 (Spec 032 / parent MCP-2916): the server Configuration tab exposes an
7+
// "Auto-approve tool changes" toggle bound to the per-server
8+
// `auto_approve_tool_changes` config flag (MCP-2930). Enabling it disables
9+
// rug-pull protection for that server, so a ⚠️ warning hint sits beneath it.
10+
// Toggling persists through the existing PATCH /api/v1/servers/{id} path.
11+
12+
let serverAutoApprove = false
13+
14+
vi.mock('@/services/api', () => {
15+
const ok = (data: unknown = {}) => Promise.resolve({ success: true, data })
16+
return {
17+
default: {
18+
getServers: vi.fn(() =>
19+
ok({
20+
servers: [
21+
{
22+
name: 'github',
23+
protocol: 'stdio',
24+
enabled: true,
25+
connected: true,
26+
quarantined: false,
27+
tool_count: 1,
28+
auto_approve_tool_changes: serverAutoApprove,
29+
},
30+
],
31+
})
32+
),
33+
getToolApprovals: vi.fn(() => ok({ tools: [], count: 0 })),
34+
getToolDiff: vi.fn(() => ok({})),
35+
getServerTools: vi.fn(() => ok({ tools: [] })),
36+
getSecurityOverview: vi.fn(() => ok({})),
37+
listScanners: vi.fn(() => ok({ scanners: [] })),
38+
getServerLogs: vi.fn(() => ok({ logs: [] })),
39+
discoverServerTools: vi.fn(() => ok({})),
40+
patchServer: vi.fn(() => ok({})),
41+
},
42+
}
43+
})
44+
45+
async function mountDetail() {
46+
const api = (await import('@/services/api')).default
47+
const ServerDetail = (await import('@/views/ServerDetail.vue')).default
48+
const router = createRouter({
49+
history: createWebHistory(),
50+
routes: [{ path: '/servers/:serverName', component: { template: '<div/>' } }],
51+
})
52+
await router.push('/servers/github?tab=config')
53+
await router.isReady()
54+
const wrapper = mount(ServerDetail, {
55+
props: { serverName: 'github' },
56+
global: { plugins: [createPinia(), router] },
57+
})
58+
await flushPromises()
59+
return { wrapper, api }
60+
}
61+
62+
describe('ServerDetail — Auto-approve tool changes (MCP-2932)', () => {
63+
beforeEach(() => {
64+
setActivePinia(createPinia())
65+
serverAutoApprove = false
66+
})
67+
68+
it('renders the auto-approve checkbox and rug-pull warning hint', async () => {
69+
const { wrapper } = await mountDetail()
70+
expect(wrapper.find('[data-test="auto-approve-tool-changes"]').exists()).toBe(true)
71+
expect(wrapper.find('[data-test="auto-approve-warning"]').exists()).toBe(true)
72+
})
73+
74+
it('checkbox reflects the server auto_approve_tool_changes value (off by default)', async () => {
75+
const { wrapper } = await mountDetail()
76+
const cb = wrapper.find('[data-test="auto-approve-tool-changes"]')
77+
.element as HTMLInputElement
78+
expect(cb.checked).toBe(false)
79+
})
80+
81+
it('checkbox reflects an enabled server flag', async () => {
82+
serverAutoApprove = true
83+
const { wrapper } = await mountDetail()
84+
const cb = wrapper.find('[data-test="auto-approve-tool-changes"]')
85+
.element as HTMLInputElement
86+
expect(cb.checked).toBe(true)
87+
})
88+
89+
it('toggling on persists via PATCH with auto_approve_tool_changes:true', async () => {
90+
const { wrapper, api } = await mountDetail()
91+
await wrapper.find('[data-test="auto-approve-tool-changes"]').setValue(true)
92+
await flushPromises()
93+
expect(api.patchServer).toHaveBeenCalledWith('github', {
94+
auto_approve_tool_changes: true,
95+
})
96+
})
97+
98+
it('toggling off persists via PATCH with auto_approve_tool_changes:false', async () => {
99+
serverAutoApprove = true
100+
const { wrapper, api } = await mountDetail()
101+
await wrapper.find('[data-test="auto-approve-tool-changes"]').setValue(false)
102+
await flushPromises()
103+
expect(api.patchServer).toHaveBeenCalledWith('github', {
104+
auto_approve_tool_changes: false,
105+
})
106+
})
107+
})

0 commit comments

Comments
 (0)