diff --git a/panel/frontend/src/App.vue b/panel/frontend/src/App.vue index 91fe629..6572967 100644 --- a/panel/frontend/src/App.vue +++ b/panel/frontend/src/App.vue @@ -34,6 +34,11 @@ onSelect: (item, event) => { event.preventDefault(); router.push(item.href) }, }, { + label: t('nav.mcpKeys'), icon: 'ph-key', href: '/mcp-keys', + active: route.path.startsWith('/mcp-keys'), + onSelect: (item, event) => { event.preventDefault(); router.push(item.href) }, + }, + { label: t('nav.settings'), icon: 'ph-gear', href: '/settings', active: route.path.startsWith('/settings'), onSelect: (item, event) => { event.preventDefault(); router.push(item.href) }, diff --git a/panel/frontend/src/api.js b/panel/frontend/src/api.js index 0ff2c72..a224e3a 100644 --- a/panel/frontend/src/api.js +++ b/panel/frontend/src/api.js @@ -82,6 +82,13 @@ }, serviceIncidents: (id, limit = 50) => request(`/services/${id}/incidents?limit=${limit}`), + // Персональные MCP-ключи (только cookie-сессия, как /me PATCH); + // createMcpToken возвращает plaintext-ключ ровно один раз + mcpTokens: () => request('/me/mcp_tokens'), + createMcpToken: (name) => request('/me/mcp_tokens', { method: 'POST', body: { name } }), + revokeMcpToken: (id) => request(`/me/mcp_tokens/${id}/revoke`, { method: 'POST' }), + adminMcpTokens: () => request('/admin/mcp_tokens'), + adminRevokeMcpToken: (id) => request(`/admin/mcp_tokens/${id}/revoke`, { method: 'POST' }), // OAuth не под /api/v1 — служебные пути панели logout: async () => { return fetch('/auth/logout', { method: 'POST' }) diff --git a/panel/frontend/src/i18n/messages/en.js b/panel/frontend/src/i18n/messages/en.js index 556fb7b..031acd3 100644 --- a/panel/frontend/src/i18n/messages/en.js +++ b/panel/frontend/src/i18n/messages/en.js @@ -111,6 +111,7 @@ // services (health checker) "nav.services": "Services", + "nav.mcpKeys": "MCP keys", "services.title": "Services", "services.subtitle": "Health of external services via their health endpoints", "services.addService": "Add service", @@ -193,4 +194,38 @@ "common.once": "once", "common.error": "Error", "common.failedToLoad": "Failed to load", + + // MCP personal keys (handbook 10-platform/mcp.md) + "mcpKeys.title": "MCP keys", + "mcpKeys.myKeys": "My keys", + "mcpKeys.myKeysNote": "Bearer tokens for your AI agents against /mcp of this panel.", + "mcpKeys.defaultName": "Agent", + "mcpKeys.namePlaceholder": "agent", + "mcpKeys.generate": "Generate key", + "mcpKeys.newKey": "New MCP key", + "mcpKeys.plaintextOnceName": "Keys are shown once after creation; only a tail hint is stored afterwards.", + "mcpKeys.createdTitle": "Key created", + "mcpKeys.plaintextHint": "Copy the key now — it will never be shown again.", + "mcpKeys.colKey": "Key", + "mcpKeys.colTail": "Tail", + "mcpKeys.colRole": "Role", + "mcpKeys.colCreated": "Created", + "mcpKeys.colUsed": "Last used", + "mcpKeys.colStatus": "Status", + "mcpKeys.colOwner": "Owner", + "mcpKeys.badgeActive": "Active", + "mcpKeys.badgeRevoked": "Revoked", + "mcpKeys.revoke": "Revoke", + "mcpKeys.revokeTitle": "Revoke key", + "mcpKeys.revokeMessage": "Agents using the \"{name}\" key will stop getting answers from /mcp.", + "mcpKeys.revokedTitle": "Key revoked", + "mcpKeys.failed": "Failed", + "mcpKeys.emptyOwn": "No keys yet — generate one for your agent.", + "mcpKeys.emptyAll": "No keys issued by users yet.", + "mcpKeys.allKeysAdmin": "Keys of all users", + "mcpKeys.allKeysNote": "All issued personal keys; you can revoke any of them.", + "mcpKeys.limits": "{count} of {limit} active keys", + "mcpKeys.howToConnect": "How to connect an agent", + "mcpKeys.connectNote": "Transport — streamable HTTP; authorize with the personal key from this page. Example for Claude Code:", + "mcpKeys.authOff": "Personal MCP keys work when gnexus-auth sign-in is enabled. In an open panel (or for scripts), use the GHARD_ADMIN_TOKEN.", }; \ No newline at end of file diff --git a/panel/frontend/src/i18n/messages/ru.js b/panel/frontend/src/i18n/messages/ru.js index ce941ac..4523be1 100644 --- a/panel/frontend/src/i18n/messages/ru.js +++ b/panel/frontend/src/i18n/messages/ru.js @@ -110,6 +110,7 @@ // services (health checker) "nav.services": "Сервисы", + "nav.mcpKeys": "MCP-ключи", "services.title": "Сервисы", "services.subtitle": "Здоровье внешних сервисов по их health-эндпоинтам", "services.addService": "Добавить сервис", @@ -192,4 +193,38 @@ "common.once": "один раз", "common.error": "Ошибка", "common.failedToLoad": "Не удалось загрузить", + + // MCP personal keys (handbook 10-platform/mcp.md) + "mcpKeys.title": "MCP-ключи", + "mcpKeys.myKeys": "Мои ключи", + "mcpKeys.myKeysNote": "Bearer-токены ваших ИИ-агентов для /mcp этой панели.", + "mcpKeys.defaultName": "Агент", + "mcpKeys.namePlaceholder": "агент", + "mcpKeys.generate": "Выдать ключ", + "mcpKeys.newKey": "Новый MCP-ключ", + "mcpKeys.plaintextOnceName": "Ключ показывается один раз при создании; дальше в списке — только хвост-хинт.", + "mcpKeys.createdTitle": "Ключ создан", + "mcpKeys.plaintextHint": "Скопируйте ключ сейчас — повторно он показан не будет.", + "mcpKeys.colKey": "Ключ", + "mcpKeys.colTail": "Хвост", + "mcpKeys.colRole": "Роль", + "mcpKeys.colCreated": "Создан", + "mcpKeys.colUsed": "Был вызван", + "mcpKeys.colStatus": "Статус", + "mcpKeys.colOwner": "Владелец", + "mcpKeys.badgeActive": "Активен", + "mcpKeys.badgeRevoked": "Отозван", + "mcpKeys.revoke": "Отозвать", + "mcpKeys.revokeTitle": "Отозвать ключ", + "mcpKeys.revokeMessage": "Агенты с ключом \"{name}\" перестанут получать ответы от /mcp.", + "mcpKeys.revokedTitle": "Ключ отозван", + "mcpKeys.failed": "Не удалось", + "mcpKeys.emptyOwn": "Ключей нет — выпустите ключ для своего агента.", + "mcpKeys.emptyAll": "Пока никто не выпускал ключей.", + "mcpKeys.allKeysAdmin": "Ключи всех пользователей", + "mcpKeys.allKeysNote": "Все выпущенные персональные ключи; любой можно отозвать.", + "mcpKeys.limits": "активных ключей: {count} из {limit}", + "mcpKeys.howToConnect": "Как подключить агент", + "mcpKeys.connectNote": "Транспорт — streamable HTTP; авторизация — персональным ключом с этой страницы. Пример для Claude Code:", + "mcpKeys.authOff": "Персональные MCP-ключи работают при включённом входе gnexus-auth. В открытой панели (и для скриптов) используйте GHARD_ADMIN_TOKEN.", }; \ No newline at end of file diff --git a/panel/frontend/src/i18n/messages/uk.js b/panel/frontend/src/i18n/messages/uk.js index bb447a5..582d064 100644 --- a/panel/frontend/src/i18n/messages/uk.js +++ b/panel/frontend/src/i18n/messages/uk.js @@ -110,6 +110,7 @@ // services (health checker) "nav.services": "Сервіси", + "nav.mcpKeys": "MCP-ключі", "services.title": "Сервіси", "services.subtitle": "Здоров'я зовнішніх сервісів за їх health-ендпоінтами", "services.addService": "Додати сервіс", @@ -192,4 +193,38 @@ "common.once": "один раз", "common.error": "Помилка", "common.failedToLoad": "Не вдалося завантажити", + + // MCP personal keys (handbook 10-platform/mcp.md) + "mcpKeys.title": "MCP-ключі", + "mcpKeys.myKeys": "Мої ключі", + "mcpKeys.myKeysNote": "Bearer-токени ваших ІІ-агентів до /mcp цієї панелі.", + "mcpKeys.defaultName": "Агент", + "mcpKeys.namePlaceholder": "агент", + "mcpKeys.generate": "Видати ключ", + "mcpKeys.newKey": "Новий MCP-ключ", + "mcpKeys.plaintextOnceName": "Ключ показується один раз при створенні; далі у списку — лише хвіст-хінт.", + "mcpKeys.createdTitle": "Ключ створено", + "mcpKeys.plaintextHint": "Скопіюйте ключ зараз — повторно він показаний не буде.", + "mcpKeys.colKey": "Ключ", + "mcpKeys.colTail": "Хвіст", + "mcpKeys.colRole": "Роль", + "mcpKeys.colCreated": "Створено", + "mcpKeys.colUsed": "Викликано", + "mcpKeys.colStatus": "Статус", + "mcpKeys.colOwner": "Власник", + "mcpKeys.badgeActive": "Активний", + "mcpKeys.badgeRevoked": "Відкликано", + "mcpKeys.revoke": "Відкликати", + "mcpKeys.revokeTitle": "Відкликати ключ", + "mcpKeys.revokeMessage": "Агенти з ключем \"{name}\" перестануть отримувати відповіді від /mcp.", + "mcpKeys.revokedTitle": "Ключ відкликано", + "mcpKeys.failed": "Не вдалося", + "mcpKeys.emptyOwn": "Ключів немає — видайте ключ для свого агента.", + "mcpKeys.emptyAll": "Поки ніхто не видавав ключів.", + "mcpKeys.allKeysAdmin": "Ключі всіх користувачів", + "mcpKeys.allKeysNote": "Усі видані персональні ключі; будь-який можна відкликати.", + "mcpKeys.limits": "активних ключів: {count} з {limit}", + "mcpKeys.howToConnect": "Як підключити агента", + "mcpKeys.connectNote": "Транспорт — streamable HTTP; авторизація — персональним ключем з цієї сторінки. Приклад для Claude Code:", + "mcpKeys.authOff": "Персональні MCP-ключі працюють за увімкненого входу gnexus-auth. У відкритій панелі (та для скриптів) використовуйте GHARD_ADMIN_TOKEN.", }; \ No newline at end of file diff --git a/panel/frontend/src/pages/McpKeysPage.vue b/panel/frontend/src/pages/McpKeysPage.vue new file mode 100644 index 0000000..54f5d4f --- /dev/null +++ b/panel/frontend/src/pages/McpKeysPage.vue @@ -0,0 +1,330 @@ + + + + + \ No newline at end of file diff --git a/panel/frontend/src/router.js b/panel/frontend/src/router.js index 245511a..8f14856 100644 --- a/panel/frontend/src/router.js +++ b/panel/frontend/src/router.js @@ -25,6 +25,11 @@ props: true, }, { + path: '/mcp-keys', + name: 'mcpKeys', + component: () => import('./pages/McpKeysPage.vue'), + }, + { path: '/settings', name: 'settings', component: () => import('./pages/SettingsPage.vue'),