Files
LocalAI/core/http/react-ui/e2e/mcp-server-status.spec.js
T
Stefan Walcz 95034f1147 feat(ui): offer model MCP servers to non-admin chat users (#12660)
The chat page derived MCP availability from GET
/api/models/config-json/:name, which is admin-only. With auth enabled a
regular user holding the `mcp` feature (default ON) therefore never saw
the MCP Servers/Prompts/Resources tabs, although every endpoint behind
them (/v1/mcp/servers|prompts|resources/:model) is gated only by that
feature.

/api/models/capabilities, which the chat already loads for its model
picker and which honours the per-user model allowlist, now reports
`mcp: true` for models whose config declares remote or stdio MCP servers
(never when MCP is disabled on the instance). For non-admins the chat
combines that flag with hasFeature('mcp') and clears a stale MCP
selection otherwise. Admins keep the config-json path unchanged; the
model info panel and automatic context size stay admin-only on purpose,
since they need the full model config.

Assisted-by: Claude Code:claude-opus-5-5

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
2026-10-19 23:43:47 +02:00

148 lines
5.4 KiB
JavaScript

import { test, expect } from './coverage-fixtures.js'
async function seedModelChat(page) {
await page.addInitScript(() => {
const now = Date.now()
localStorage.setItem('localai_chats_data', JSON.stringify({
chats: [{
id: 'mcp-status-chat',
name: 'MCP status',
model: 'test-model',
history: [],
systemPrompt: '',
mcpMode: false,
mcpServers: [],
clientMCPServers: [],
temperature: null,
topP: null,
topK: null,
tokenUsage: { prompt: 0, completion: 0, total: 0 },
contextSize: null,
createdAt: now,
updatedAt: now,
}],
activeChatId: 'mcp-status-chat',
lastSaved: now,
}))
})
}
async function mockMCPModel(page) {
await page.route('**/api/models/capabilities', route => route.fulfill({
json: { data: [{ id: 'test-model', capabilities: ['FLAG_CHAT'] }] },
}))
await page.route('**/api/models/config-json/test-model', route => route.fulfill({
json: {
name: 'test-model',
mcp: { remote: 'mcpServers:\n ordino:\n url: http://ordino:8080/mcp' },
},
}))
}
test('configured MCP server remains visible with its discovery error', async ({ page }) => {
await seedModelChat(page)
await mockMCPModel(page)
let discoveryRequests = 0
await page.route('**/v1/mcp/servers/test-model', route => {
discoveryRequests++
return route.fulfill({
json: {
model: 'test-model',
servers: [{
name: 'ordino',
type: 'remote',
tools: [],
error: 'connection failed: lookup ordino.internal: no such host',
}],
},
})
})
await page.goto('/app/chat')
await expect(page.getByRole('button', { name: 'test-model' })).toBeVisible({ timeout: 10_000 })
await page.locator('.chat-mcp-dropdown > button').click()
await page.getByRole('button', { name: 'Servers', exact: true }).click()
const server = page.locator('.chat-mcp-server-item', { hasText: 'ordino' })
await expect(server).toBeVisible()
await expect(server).toContainText('lookup ordino.internal: no such host')
await expect(server.locator('.chat-mcp-server-status--error')).toBeVisible()
await expect(server.getByRole('checkbox')).toBeDisabled()
await page.getByRole('button', { name: 'Client', exact: true }).click()
await page.getByRole('button', { name: 'Servers', exact: true }).click()
await expect.poll(() => discoveryRequests).toBeGreaterThanOrEqual(2)
})
test('server-list request errors are shown in the MCP menu', async ({ page }) => {
await seedModelChat(page)
await mockMCPModel(page)
await page.route('**/v1/mcp/servers/test-model', route => route.fulfill({
status: 500,
json: { message: 'invalid MCP configuration: missing mcpServers map' },
}))
await page.goto('/app/chat')
await expect(page.getByRole('button', { name: 'test-model' })).toBeVisible({ timeout: 10_000 })
await page.locator('.chat-mcp-dropdown > button').click()
await page.getByRole('button', { name: 'Servers', exact: true }).click()
await expect(page.getByRole('alert')).toContainText('invalid MCP configuration: missing mcpServers map')
})
// Non-admin users cannot read /api/models/config-json (admin-only). Their MCP
// availability comes from the `mcp` flag of /api/models/capabilities plus the
// user's `mcp` feature permission.
async function mockNonAdmin(page, permissions) {
await page.route('**/api/auth/status', route => route.fulfill({
json: {
authEnabled: true,
staticApiKeyRequired: false,
providers: ['local'],
hasUsers: true,
user: { id: 'u-member', name: 'Member', email: 'member@example.com', role: 'user', permissions },
},
}))
let configJsonRequests = 0
await page.route('**/api/models/config-json/**', route => {
configJsonRequests++
return route.fulfill({ status: 403, json: { error: 'admin access required' } })
})
await page.route('**/api/models/capabilities', route => route.fulfill({
json: { data: [{ id: 'test-model', capabilities: ['FLAG_CHAT'], mcp: true }] },
}))
await page.route('**/v1/mcp/servers/test-model', route => route.fulfill({
json: { model: 'test-model', servers: [{ name: 'weather', type: 'remote', tools: ['get_weather'] }] },
}))
return () => configJsonRequests
}
test('non-admin user with the mcp feature can select model MCP servers', async ({ page }) => {
await seedModelChat(page)
const configJsonRequests = await mockNonAdmin(page, { chat: true, mcp: true })
await page.goto('/app/chat')
await expect(page.getByRole('button', { name: 'test-model' })).toBeVisible({ timeout: 10_000 })
await page.locator('.chat-mcp-dropdown > button').click()
await page.getByRole('button', { name: 'Servers', exact: true }).click()
const server = page.locator('.chat-mcp-server-item', { hasText: 'weather' })
await expect(server).toBeVisible()
await expect(server.getByRole('checkbox')).toBeEnabled()
expect(configJsonRequests()).toBe(0)
})
test('non-admin user without the mcp feature gets no model MCP servers', async ({ page }) => {
await seedModelChat(page)
await mockNonAdmin(page, { chat: true, mcp: false })
await page.goto('/app/chat')
await expect(page.getByRole('button', { name: 'test-model' })).toBeVisible({ timeout: 10_000 })
await page.locator('.chat-mcp-dropdown > button').click()
await expect(page.getByRole('button', { name: 'Client', exact: true })).toBeVisible()
await expect(page.getByRole('button', { name: 'Servers', exact: true })).toHaveCount(0)
})