mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 17:18:49 -04:00
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>
148 lines
5.4 KiB
JavaScript
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)
|
|
})
|