diff --git a/docs/features/tool-quarantine.md b/docs/features/tool-quarantine.md index 45fd9343..c1c2b18d 100644 --- a/docs/features/tool-quarantine.md +++ b/docs/features/tool-quarantine.md @@ -272,6 +272,16 @@ appear at once. The server list page shows a quarantine badge with the count of pending/changed tools for each server. +The server detail view's **Configuration** tab carries an **Auto-approve tool +changes** toggle (a **Tool-change approval** card) bound to the per-server +`auto_approve_tool_changes` flag. It is **OFF by default (protected)**; a ⚠️ hint +beneath it explains that enabling it disables rug-pull protection for that +server — future tool description/schema changes and newly added tools are then +trusted automatically instead of held for review. Toggling persists through the +existing `PATCH /api/v1/servers/{id}` path. (As noted under +[Per-Server Auto-Approve](#per-server-auto-approve), the runtime enforcement of +this flag rolls out in an upcoming release.) + ### Doctor Command The `mcpproxy doctor` command includes a "Tools Pending Quarantine Approval" section: diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 5db7e805..f7757dfc 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -160,6 +160,12 @@ export interface Server { protocol: 'http' | 'stdio' | 'streamable-http' enabled: boolean quarantined: boolean + // Per-server intent to auto-approve tool changes/additions (MCP-2930). + // When true, rug-pull protection is disabled for this server: changed and + // newly-added tools are trusted automatically instead of held for review. + // Optional because the REST status payload only includes it once the + // backend exposes the flag; absent/undefined is treated as OFF (protected). + auto_approve_tool_changes?: boolean connected: boolean connecting: boolean authenticated?: boolean diff --git a/frontend/src/views/ServerDetail.vue b/frontend/src/views/ServerDetail.vue index d04e7f33..3d258d17 100644 --- a/frontend/src/views/ServerDetail.vue +++ b/frontend/src/views/ServerDetail.vue @@ -705,6 +705,60 @@ + +
+
+

Tool-change approval

+ + + +

+ + + Enabling this disables rug-pull protection: changed + tool descriptions/schemas and newly added tools will be trusted + automatically instead of held for review. Protected (default) is + recommended. + +

+
+
+
@@ -2617,6 +2671,27 @@ function scopeKey(scope: 'header' | 'env'): 'headers' | 'env' { return scope === 'header' ? 'headers' : 'env' } +// MCP-2932: per-server "Auto-approve tool changes" toggle. Absent/undefined on +// the status payload is treated as OFF (protected) — see the Server type note. +const autoApproveToolChanges = computed(() => server.value?.auto_approve_tool_changes ?? false) + +async function toggleAutoApproveToolChanges(event: Event) { + const checked = (event.target as HTMLInputElement).checked + // Persist through the existing PATCH /api/v1/servers/{id} path. The backend + // auto-approves changed/added tools on the next discovery pass for this + // server (MCP-2931); patchServerDiff surfaces the success toast. + const ok = await patchServerDiff( + { auto_approve_tool_changes: checked }, + checked ? 'Auto-approve tool changes enabled' : 'Auto-approve tool changes disabled' + ) + // On failure, snap the checkbox back to the persisted value: patchServerDiff + // refetches servers on success, so the bound computed already reflects truth; + // an explicit no-op here keeps the control consistent with `server`. + if (!ok && event.target) { + ;(event.target as HTMLInputElement).checked = autoApproveToolChanges.value + } +} + async function saveEdit(scope: 'header' | 'env', k: string, val: string) { const ok = await patchServerDiff({ [scopeKey(scope)]: { [k]: val } }, `Updated ${k}`) if (ok) editingKey.value = null diff --git a/frontend/tests/unit/server-detail-auto-approve.spec.ts b/frontend/tests/unit/server-detail-auto-approve.spec.ts new file mode 100644 index 00000000..f31a28e1 --- /dev/null +++ b/frontend/tests/unit/server-detail-auto-approve.spec.ts @@ -0,0 +1,107 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { createRouter, createWebHistory } from 'vue-router' + +// MCP-2932 (Spec 032 / parent MCP-2916): the server Configuration tab exposes an +// "Auto-approve tool changes" toggle bound to the per-server +// `auto_approve_tool_changes` config flag (MCP-2930). Enabling it disables +// rug-pull protection for that server, so a ⚠️ warning hint sits beneath it. +// Toggling persists through the existing PATCH /api/v1/servers/{id} path. + +let serverAutoApprove = false + +vi.mock('@/services/api', () => { + const ok = (data: unknown = {}) => Promise.resolve({ success: true, data }) + return { + default: { + getServers: vi.fn(() => + ok({ + servers: [ + { + name: 'github', + protocol: 'stdio', + enabled: true, + connected: true, + quarantined: false, + tool_count: 1, + auto_approve_tool_changes: serverAutoApprove, + }, + ], + }) + ), + getToolApprovals: vi.fn(() => ok({ tools: [], count: 0 })), + getToolDiff: vi.fn(() => ok({})), + getServerTools: vi.fn(() => ok({ tools: [] })), + getSecurityOverview: vi.fn(() => ok({})), + listScanners: vi.fn(() => ok({ scanners: [] })), + getServerLogs: vi.fn(() => ok({ logs: [] })), + discoverServerTools: vi.fn(() => ok({})), + patchServer: vi.fn(() => ok({})), + }, + } +}) + +async function mountDetail() { + const api = (await import('@/services/api')).default + const ServerDetail = (await import('@/views/ServerDetail.vue')).default + const router = createRouter({ + history: createWebHistory(), + routes: [{ path: '/servers/:serverName', component: { template: '
' } }], + }) + await router.push('/servers/github?tab=config') + await router.isReady() + const wrapper = mount(ServerDetail, { + props: { serverName: 'github' }, + global: { plugins: [createPinia(), router] }, + }) + await flushPromises() + return { wrapper, api } +} + +describe('ServerDetail — Auto-approve tool changes (MCP-2932)', () => { + beforeEach(() => { + setActivePinia(createPinia()) + serverAutoApprove = false + }) + + it('renders the auto-approve checkbox and rug-pull warning hint', async () => { + const { wrapper } = await mountDetail() + expect(wrapper.find('[data-test="auto-approve-tool-changes"]').exists()).toBe(true) + expect(wrapper.find('[data-test="auto-approve-warning"]').exists()).toBe(true) + }) + + it('checkbox reflects the server auto_approve_tool_changes value (off by default)', async () => { + const { wrapper } = await mountDetail() + const cb = wrapper.find('[data-test="auto-approve-tool-changes"]') + .element as HTMLInputElement + expect(cb.checked).toBe(false) + }) + + it('checkbox reflects an enabled server flag', async () => { + serverAutoApprove = true + const { wrapper } = await mountDetail() + const cb = wrapper.find('[data-test="auto-approve-tool-changes"]') + .element as HTMLInputElement + expect(cb.checked).toBe(true) + }) + + it('toggling on persists via PATCH with auto_approve_tool_changes:true', async () => { + const { wrapper, api } = await mountDetail() + await wrapper.find('[data-test="auto-approve-tool-changes"]').setValue(true) + await flushPromises() + expect(api.patchServer).toHaveBeenCalledWith('github', { + auto_approve_tool_changes: true, + }) + }) + + it('toggling off persists via PATCH with auto_approve_tool_changes:false', async () => { + serverAutoApprove = true + const { wrapper, api } = await mountDetail() + await wrapper.find('[data-test="auto-approve-tool-changes"]').setValue(false) + await flushPromises() + expect(api.patchServer).toHaveBeenCalledWith('github', { + auto_approve_tool_changes: false, + }) + }) +})