fix: improve MCP page setup UX and remove automation credit quotas (#821)

* Remove MCP credit quotas and improve MCP setup page

- Always show client setup snippets with placeholder keys
- Add View Docs link to MCP page header
- Remove monthly automation credit enforcement and UI
- Update plan feature text to reflect free MCP access

Co-authored-by: Rotimi Best <[email protected]>

* fix: address PR review feedback for MCP page changes

- Clear generatedSecret when revoking MCP keys
- Use Button link mode for View Docs with rel noopener noreferrer
- Update all dashboard locale files for MCP translations
- Make MCP rate-limit admission atomic with transactional reservations
- Wrap MCP route handlers with withMcpAutomationUsage helper

Co-authored-by: Rotimi Best <[email protected]>

* revert: remove atomic MCP rate-limit reservations

The transactional advisory-lock reservation was unnecessary complexity
for our MCP usage model. Restore the simpler check-then-record flow.

Also only clear generatedSecret when revoking the key it belongs to.

Co-authored-by: Rotimi Best <[email protected]>

* fix: address remaining MCP page bot review feedback

- Add blank lines after confirm guards in key handlers
- Skip showing rotated secret if key was revoked during rotation
- Fix vi keys_title and de rate_limits_title translations

Co-authored-by: Rotimi Best <[email protected]>

* fix: prevent late key rotation from un-revoking or exposing secrets

- Rotate only when revoked_at IS NULL at update time (DB)
- Reject rotate requests for already-revoked keys (API)
- Ignore stale rotate responses in automation client state
- Guard MCP UI against showing secrets for revoked keys

Co-authored-by: Rotimi Best <[email protected]>

---------
This commit is contained in:
Rotimi Best
2026-08-16 22:12:04 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 1cb752d229
commit 9fb75e11d2
18 changed files with 120 additions and 94 deletions
@@ -13,6 +13,7 @@ import {
getActiveOrganizationApiKeyByHash,
getOrganizationApiKeyById,
listOrganizationApiKeys,
rotateOrganizationApiKeyIfActive,
updateOrganizationApiKey
} from '@cio/db/queries/organization';
import { assertOrganizationAutomationKeyCreationAllowed } from './automation-usage';
@@ -135,16 +136,26 @@ export async function rotateOrganizationApiKeyService(organizationId: string, ke
throw new AppError('Organization API key not found', ErrorCodes.ORGANIZATION_API_KEY_NOT_FOUND, 404);
}
if (existing.revokedAt) {
throw new AppError(
'Cannot rotate a revoked organization API key',
ErrorCodes.ORGANIZATION_API_KEY_ROTATE_FAILED,
400
);
}
const secret = generateOrganizationApiKeySecret(existing.type);
const updated = await updateOrganizationApiKey(organizationId, keyId, {
const updated = await rotateOrganizationApiKeyIfActive(organizationId, keyId, {
secretPrefix: getSecretPrefix(secret),
secretHash: hashSecret(secret),
revokedAt: null,
lastUsedAt: null
secretHash: hashSecret(secret)
});
if (!updated) {
throw new AppError('Failed to rotate organization API key', ErrorCodes.ORGANIZATION_API_KEY_ROTATE_FAILED, 500);
throw new AppError(
'Organization API key was revoked before rotation completed',
ErrorCodes.ORGANIZATION_API_KEY_ROTATE_FAILED,
409
);
}
return {
@@ -6,7 +6,6 @@ import {
countOrganizationAutomationUsageSinceByKey,
createOrganizationAutomationUsage,
getActiveOrganizationPlan,
getOrganizationAutomationCreditsUsedSince,
listRecentOrganizationAutomationUsage
} from '@cio/db/queries/organization';
import type { TOrganizationApiKey, TOrganizationApiKeyType, TPlan } from '@db/types';
@@ -15,7 +14,6 @@ import {
getMcpAutomationCategory,
canUsePublicApi,
getMcpAutomationLimits,
MCP_TOOL_CREDIT_COST,
type TMcpToolName
} from '@cio/utils/plans';
@@ -24,9 +22,6 @@ export type OrganizationAutomationUsageSummary = {
planName: TPlan;
billingPeriodStart: string;
billingPeriodEnd: string;
monthlyCreditsIncluded: number;
monthlyCreditsUsed: number;
monthlyCreditsRemaining: number;
activeKeys: number;
maxActiveKeys: number;
rateLimits: ReturnType<typeof getMcpAutomationLimits>['rateLimits'];
@@ -104,9 +99,6 @@ export async function getOrganizationAutomationUsageSummaryService(
planName,
billingPeriodStart: billingPeriodStart.toISOString(),
billingPeriodEnd: billingPeriodEnd.toISOString(),
monthlyCreditsIncluded: 0,
monthlyCreditsUsed: 0,
monthlyCreditsRemaining: 0,
activeKeys: await countActiveOrganizationApiKeys(organizationId, type),
maxActiveKeys: 0,
rateLimits: {
@@ -118,8 +110,7 @@ export async function getOrganizationAutomationUsageSummaryService(
}
const limits = getMcpAutomationLimits(planName);
const [monthlyCreditsUsed, activeKeys, recentActions] = await Promise.all([
getOrganizationAutomationCreditsUsedSince(organizationId, type, billingPeriodStart.toISOString()),
const [activeKeys, recentActions] = await Promise.all([
countActiveOrganizationApiKeys(organizationId, type),
listRecentOrganizationAutomationUsage(organizationId, type, 10)
]);
@@ -129,9 +120,6 @@ export async function getOrganizationAutomationUsageSummaryService(
planName,
billingPeriodStart: billingPeriodStart.toISOString(),
billingPeriodEnd: billingPeriodEnd.toISOString(),
monthlyCreditsIncluded: limits.monthlyCredits,
monthlyCreditsUsed,
monthlyCreditsRemaining: Math.max(limits.monthlyCredits - monthlyCreditsUsed, 0),
activeKeys,
maxActiveKeys: limits.maxActiveKeys,
rateLimits: limits.rateLimits,
@@ -151,24 +139,13 @@ export async function assertMcpAutomationUsageAllowed(
const limits = getMcpAutomationLimits(planName);
const category = getMcpAutomationCategory(toolName);
const now = new Date();
const billingPeriodStart = getBillingPeriodStart(now).toISOString();
const minuteWindowStart = getMinuteWindowStart(now).toISOString();
const [creditsUsed, keyRequestsInWindow, orgRequestsInWindow] = await Promise.all([
getOrganizationAutomationCreditsUsedSince(automationKey.organizationId, automationKey.type, billingPeriodStart),
const [keyRequestsInWindow, orgRequestsInWindow] = await Promise.all([
countOrganizationAutomationUsageSinceByKey(automationKey.id, category, minuteWindowStart),
countOrganizationAutomationUsageSince(automationKey.organizationId, automationKey.type, category, minuteWindowStart)
]);
const creditsNeeded = MCP_TOOL_CREDIT_COST[toolName];
if (creditsUsed + creditsNeeded > limits.monthlyCredits) {
throw new AppError(
'Monthly automation credits exhausted for this organization',
ErrorCodes.AUTOMATION_CREDIT_LIMIT_EXCEEDED,
429
);
}
const keyLimit =
category === 'read'
? limits.rateLimits.perKey.readPerMinute
@@ -199,7 +176,7 @@ export async function recordMcpAutomationUsage(
type: automationKey.type,
action: toolName,
category: getMcpAutomationCategory(toolName),
creditsConsumed: MCP_TOOL_CREDIT_COST[toolName],
creditsConsumed: 0,
metadata
});
}
@@ -99,6 +99,12 @@ class AutomationApi extends BaseApiWithErrors {
requestFn: () => classroomio.organization.automation.keys[':keyId'].rotate.$post({ param: { keyId } }),
logContext: 'rotating automation key',
onSuccess: (response) => {
const existingKey = this.keys.find((key) => key.id === response.data.key.id);
if (existingKey?.revokedAt) {
this.generatedSecret = null;
return;
}
this.generatedSecret = response.data.secret;
this.keys = this.keys.map((key) => (key.id === response.data.key.id ? response.data.key : key));
snackbar.success('snackbar.automation.rotated');
@@ -21,7 +21,6 @@
import * as Tabs from '@cio/ui/base/tabs';
import { InputField } from '@cio/ui/custom/input-field';
import { IconButton } from '@cio/ui/custom/icon-button';
import CoinsIcon from '@lucide/svelte/icons/coins';
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
import GaugeIcon from '@lucide/svelte/icons/gauge';
import KeyIcon from '@lucide/svelte/icons/key';
@@ -32,6 +31,7 @@
let activeSetupTab = $state('cursor');
let generatedSecret = $state<string | null>(null);
let generatedSecretKeyId = $state<string | null>(null);
let isCreateKeyModalOpen = $state(false);
let keyLabel = $state('');
@@ -52,6 +52,7 @@
if (!result) return;
generatedSecret = automationApi.generatedSecret;
generatedSecretKeyId = result.key.id;
automationApi.clearGeneratedSecret();
isCreateKeyModalOpen = false;
resetCreateKeyModal();
@@ -59,13 +60,25 @@
async function onRevokeKey(keyId: string) {
if (!confirm(t.get('automation.keys.revoke_confirm'))) return;
await automationApi.revokeKey(keyId);
const result = await automationApi.revokeKey(keyId);
if (result && generatedSecretKeyId === keyId) {
generatedSecret = null;
generatedSecretKeyId = null;
}
}
async function onRotateKey(keyId: string) {
if (!confirm(t.get('automation.keys.rotate_confirm'))) return;
await automationApi.rotateKey(keyId);
const result = await automationApi.rotateKey(keyId);
if (!result || result.key.revokedAt) return;
const key = automationApi.keys.find((entry) => entry.id === keyId);
if (!key || key.revokedAt) return;
generatedSecret = automationApi.generatedSecret;
generatedSecretKeyId = keyId;
automationApi.clearGeneratedSecret();
}
const mcpKeys = $derived(automationApi.keys.filter((key) => key.type === 'mcp'));
@@ -73,13 +86,6 @@
if (!automationApi.usage) return [];
return [
{
icon: CoinsIcon,
title: t.get('automation.mcp.usage.credits_title'),
percentage: automationApi.usage.monthlyCreditsRemaining,
description: `${t.get('automation.mcp.usage.used_label')} ${automationApi.usage.monthlyCreditsUsed} / ${automationApi.usage.monthlyCreditsIncluded}`,
hidePercentage: true
},
{
icon: KeyIcon,
title: t.get('automation.mcp.usage.keys_title'),
@@ -98,7 +104,6 @@
});
const isLastUsageCard = (index: number) => index === usageCards.length - 1;
const hasActiveMcpKey = $derived(mcpKeys.some((key) => !key.revokedAt));
</script>
<Field.Group class="mx-auto w-full space-y-2">
@@ -112,7 +117,7 @@
{/if}
{#if $isOrgAdmin && automationApi.usage}
<div class="grid gap-4 md:grid-cols-3">
<div class="grid gap-4 md:grid-cols-2">
{#each usageCards as card, i (i)}
<ActivityCard
activity={card}
@@ -228,7 +233,6 @@
{/if}
</div>
{#if hasActiveMcpKey}
<Field.Set class="gap-3!">
<Field.Legend class="flex items-center gap-2">
<SparklesIcon class="size-5" />
@@ -277,7 +281,6 @@
</Tabs.Content>
</Tabs.Root>
</Field.Set>
{/if}
</Field.Group>
<Dialog.Root
@@ -4081,16 +4081,15 @@
},
"usage": {
"title": "Anvendelse og grænser",
"description": "MCP er tilgængelig på alle abonnementer, men brugen styres med månedlige automatiseringskreditter og nøglegrænser.",
"credits_title": "Kreditter tilbage",
"credits_description": "Muterende MCP-handlinger bruger kreditter. Læsehandlinger er gratis.",
"description": "MCP er tilgængelig på alle abonnementer. Brugen styres af nøglegrænser og hastighedsgrænser.",
"keys_title": "Aktive nøgler",
"keys_description": "Brug én MCP-nøgle pr. integration, så adgang kan roteres eller tilbagekaldes rent.",
"rate_limits_title": "Satsgrænser",
"rate_limits_description": "Læs/skriv/publicér anmodninger tilladt pr. minut.",
"used_label": "Brugt:"
},
"page_subtitle": "Forbind AI-værktøjer til ClassroomIO med organisationsnøgler, du kan rotere når som helst."
"page_subtitle": "Forbind AI-værktøjer til ClassroomIO med organisationsnøgler, du kan rotere når som helst.",
"view_docs": "Se dokumentation"
},
"api": {
"subtitle": "Administrer API-nøgler til integration med ClassroomIOs offentlige API.",
@@ -4081,16 +4081,15 @@
},
"usage": {
"title": "Nutzung und Grenzen",
"description": "MCP ist in jedem Plan verfügbar, die Nutzung wird jedoch durch monatliche Automatisierungsguthaben und Schlüssellimits gesteuert.",
"credits_title": "Verbleibende Credits",
"credits_description": "Mutierende MCP-Aktionen verbrauchen Credits. Leseaktionen sind kostenlos.",
"description": "MCP ist in jedem Plan verfügbar. Die Nutzung wird durch Schlüssellimits und Ratenlimits gesteuert.",
"keys_title": "Aktive Schlüssel",
"keys_description": "Verwenden Sie einen MCP-Schlüssel pro Integration, damit der Zugriff sauber rotiert oder widerrufen werden kann.",
"rate_limits_title": "Tarifbegrenzungen",
"rate_limits_title": "Anfragenlimits pro Minute",
"rate_limits_description": "Zulässige Lese-/Schreib-/Veröffentlichungsanfragen pro Minute.",
"used_label": "Verwendet:"
},
"page_subtitle": "Verbinden Sie KI-Tools mit ClassroomIO über organisationsbezogene Schlüssel, die Sie jederzeit rotieren können."
"page_subtitle": "Verbinden Sie KI-Tools mit ClassroomIO über organisationsbezogene Schlüssel, die Sie jederzeit rotieren können.",
"view_docs": "Dokumentation anzeigen"
},
"api": {
"subtitle": "Verwalten Sie API-Schlüssel zur Integration mit der öffentlichen ClassroomIO-API.",
@@ -4015,9 +4015,7 @@
"page_subtitle": "Connect AI tools to ClassroomIO with org-scoped keys you can rotate anytime.",
"usage": {
"title": "Usage and limits",
"description": "MCP is available on every plan, but usage is controlled with monthly automation credits and key limits.",
"credits_title": "Credits remaining",
"credits_description": "Mutating MCP actions consume credits. Read actions are free.",
"description": "MCP is available on every plan. Usage is controlled by key limits and rate limits.",
"keys_title": "Active keys",
"keys_description": "Use one MCP key per integration so access can be rotated or revoked cleanly.",
"rate_limits_title": "Rate limits",
@@ -4045,7 +4043,8 @@
"codex_description": "Adds the ClassroomIO MCP server to Codex with the same org-scoped key.",
"cursor_description": "Paste this into `~/.cursor/mcp.json` or your workspace MCP config.",
"opencode_description": "Add this to `opencode.json` in your project root or `~/.config/opencode/opencode.json`."
}
},
"view_docs": "View Docs"
},
"api": {
"subtitle": "Manage API keys to integrate with the ClassroomIO public API.",
@@ -4081,16 +4081,15 @@
},
"usage": {
"title": "Uso y límites",
"description": "MCP está disponible en todos los planes, pero el uso se controla con créditos de automatización mensuales y límites clave.",
"credits_title": "Créditos restantes",
"credits_description": "Las acciones mutantes de MCP consumen créditos. Las acciones de lectura son gratuitas.",
"description": "MCP está disponible en todos los planes. El uso se controla mediante límites de claves y límites de tasa.",
"keys_title": "Claves activas",
"keys_description": "Utilice una clave MCP por integración para que el acceso se pueda rotar o revocar limpiamente.",
"rate_limits_title": "Límites de tarifas",
"rate_limits_description": "Solicitudes de lectura/escritura/publicación permitidas por minuto.",
"used_label": "Usado:"
},
"page_subtitle": "Conecta herramientas de IA a ClassroomIO con claves con ámbito de organización que puedes rotar en cualquier momento."
"page_subtitle": "Conecta herramientas de IA a ClassroomIO con claves con ámbito de organización que puedes rotar en cualquier momento.",
"view_docs": "Ver documentación"
},
"api": {
"subtitle": "Administra las claves API para integrarte con la API pública de ClassroomIO.",
@@ -4081,16 +4081,15 @@
},
"usage": {
"title": "Utilisation et limites",
"description": "MCP est disponible sur chaque forfait, mais l'utilisation est contrôlée par des crédits d'automatisation mensuels et des limites clés.",
"credits_title": "Crédits restants",
"credits_description": "Les actions MCP en mutation consomment des crédits. Les actions de lecture sont gratuites.",
"description": "MCP est disponible sur tous les forfaits. L'utilisation est contrôlée par les limites de clés et les limites de taux.",
"keys_title": "Clés actives",
"keys_description": "Utilisez une clé MCP par intégration afin que l'accès puisse être alterné ou révoqué proprement.",
"rate_limits_title": "Limites de taux",
"rate_limits_description": "Requêtes de lecture/écriture/publication autorisées par minute.",
"used_label": "Utilisé :"
},
"page_subtitle": "Connectez des outils IA à ClassroomIO avec des clés à l'échelle de l'organisation que vous pouvez faire pivoter à tout moment."
"page_subtitle": "Connectez des outils IA à ClassroomIO avec des clés à l'échelle de l'organisation que vous pouvez faire pivoter à tout moment.",
"view_docs": "Voir la documentation"
},
"api": {
"subtitle": "Gérez les clés API pour intégrer l'API publique ClassroomIO.",
@@ -4081,16 +4081,15 @@
},
"usage": {
"title": "उपयोग और सीमाएँ",
"description": "एमसीपी हर योजना पर उपलब्ध है, लेकिन उपयोग को मासिक स्वचालन क्रेडिट और प्रमुख सीमाओं के साथ नियंत्रित किया जाता है।",
"credits_title": "क्रेडिट शेष",
"credits_description": "MCP क्रियाओं को बदलने से क्रेडिट की खपत होती है। पढ़ें क्रियाएँ निःशुल्क हैं।",
"description": "एमसीपी हर योजना पर उपलब्ध है। उपयोग कुंजी सीमाओं और दर सीमाओं से नियंत्रित होता है।",
"keys_title": "सक्रिय कुंजियाँ",
"keys_description": "प्रति एकीकरण एक एमसीपी कुंजी का उपयोग करें ताकि पहुंच को घुमाया जा सके या सफाई से रद्द किया जा सके।",
"rate_limits_title": "दर सीमा",
"rate_limits_description": "प्रति मिनट पढ़ने/लिखने/प्रकाशित करने के अनुरोधों की अनुमति है।",
"used_label": "प्रयुक्त:"
},
"page_subtitle": "किसी भी समय घुमाने योग्य संगठन-स्तरीय कुंजियों के साथ AI टूल्स को ClassroomIO से कनेक्ट करें।"
"page_subtitle": "किसी भी समय घुमाने योग्य संगठन-स्तरीय कुंजियों के साथ AI टूल्स को ClassroomIO से कनेक्ट करें।",
"view_docs": "दस्तावेज़ देखें"
},
"api": {
"subtitle": "ClassroomIO सार्वजनिक API के साथ एकीकृत करने के लिए API कुंजियाँ प्रबंधित करें।",
@@ -4081,16 +4081,15 @@
},
"usage": {
"title": "Zastosowanie i ograniczenia",
"description": "MCP jest dostępne w każdym planie, ale wykorzystanie jest kontrolowane za pomocą miesięcznych kredytów automatyzacji i limitów kluczowych.",
"credits_title": "Pozostały kredyty",
"credits_description": "Mutowanie działań MCP zużywa kredyty. Czytanie akcji jest bezpłatne.",
"description": "MCP jest dostępne w każdym planie. Wykorzystanie jest kontrolowane przez limity kluczy i limity żądań.",
"keys_title": "Aktywne klucze",
"keys_description": "Użyj jednego klucza MCP na każdą integrację, aby umożliwić łatwą rotację lub cofnięcie dostępu.",
"rate_limits_title": "Limity stawek",
"rate_limits_description": "Żądania odczytu/zapisu/publikowania dozwolone na minutę.",
"used_label": "Używane:"
},
"page_subtitle": "Połącz narzędzia AI z ClassroomIO za pomocą kluczy o zasięgu organizacji, które możesz w dowolnym momencie obrócić."
"page_subtitle": "Połącz narzędzia AI z ClassroomIO za pomocą kluczy o zasięgu organizacji, które możesz w dowolnym momencie obrócić.",
"view_docs": "Zobacz dokumentację"
},
"api": {
"subtitle": "Zarządzaj kluczami API, aby integrować się z publicznym API ClassroomIO.",
@@ -4081,16 +4081,15 @@
},
"usage": {
"title": "Uso e limites",
"description": "O MCP está disponível em todos os planos, mas o uso é controlado com créditos mensais de automação e limites principais.",
"credits_title": "Créditos restantes",
"credits_description": "As ações mutantes do MCP consomem créditos. As ações de leitura são gratuitas.",
"description": "O MCP está disponível em todos os planos. O uso é controlado por limites de chaves e limites de taxa.",
"keys_title": "Chaves ativas",
"keys_description": "Use uma chave MCP por integração para que o acesso possa ser alternado ou revogado de forma limpa.",
"rate_limits_title": "Limites de taxa",
"rate_limits_description": "Solicitações de leitura/gravação/publicação permitidas por minuto.",
"used_label": "Usado:"
},
"page_subtitle": "Conecte ferramentas de IA ao ClassroomIO com chaves da organização que você pode rotacionar a qualquer momento."
"page_subtitle": "Conecte ferramentas de IA ao ClassroomIO com chaves da organização que você pode rotacionar a qualquer momento.",
"view_docs": "Ver documentação"
},
"api": {
"subtitle": "Gerencie chaves de API para integrar com a API pública do ClassroomIO.",
@@ -4081,16 +4081,15 @@
},
"usage": {
"title": "Использование и ограничения",
"description": "MCP доступен в каждом плане, но использование контролируется с помощью ежемесячных кредитов на автоматизацию и ключевых лимитов.",
"credits_title": "Оставшиеся кредиты",
"credits_description": "Мутирующие действия MCP потребляют кредиты. Действия чтения бесплатны.",
"description": "MCP доступен в каждом плане. Использование контролируется лимитами ключей и лимитами запросов.",
"keys_title": "Активные ключи",
"keys_description": "Используйте один ключ MCP для каждой интеграции, чтобы можно было без проблем менять или отзывать доступ.",
"rate_limits_title": "Ограничения ставок",
"rate_limits_description": "Запросы на чтение/запись/публикацию разрешены в минуту.",
"used_label": "Использовано:"
},
"page_subtitle": "Подключайте ИИ-инструменты к ClassroomIO с помощью ключей организации, которые можно менять в любое время."
"page_subtitle": "Подключайте ИИ-инструменты к ClassroomIO с помощью ключей организации, которые можно менять в любое время.",
"view_docs": "Документация"
},
"api": {
"subtitle": "Управляйте API-ключами для интеграции с публичным API ClassroomIO.",
@@ -4081,16 +4081,15 @@
},
"usage": {
"title": "Cách sử dụng và giới hạn",
"description": "MCP có sẵn trên mọi gói, nhưng việc sử dụng được kiểm soát bằng tín dụng tự động hóa hàng tháng và các giới hạn chính.",
"credits_title": "Tín dụng còn lại",
"credits_description": "Các hành động MCP đột biến sẽ tiêu tốn tín dụng. Hành động đọc là miễn phí.",
"keys_title": "Phím hoạt động",
"description": "MCP có sẵn trên mọi gói. Việc sử dụng được kiểm soát bằng giới hạn khóa và giới hạn tần suất.",
"keys_title": "Khóa đang hoạt động",
"keys_description": "Sử dụng một khóa MCP cho mỗi lần tích hợp để có thể luân chuyển hoặc thu hồi quyền truy cập một cách dễ dàng.",
"rate_limits_title": "Giới hạn tỷ lệ",
"rate_limits_description": "Yêu cầu đọc/ghi/xuất bản được phép mỗi phút.",
"used_label": "Đã sử dụng:"
},
"page_subtitle": "Kết nối công cụ AI với ClassroomIO bằng khóa trong phạm vi tổ chức mà bạn có thể xoay bất cứ lúc nào."
"page_subtitle": "Kết nối công cụ AI với ClassroomIO bằng khóa trong phạm vi tổ chức mà bạn có thể xoay bất cứ lúc nào.",
"view_docs": "Xem tài liệu"
},
"api": {
"subtitle": "Quản lý khóa API để tích hợp với API công khai ClassroomIO.",
@@ -3,6 +3,8 @@
import { automationApi } from '$features/automation/api/automation.svelte';
import { t } from '$lib/utils/functions/translations';
import * as Page from '@cio/ui/base/page';
import { Button } from '@cio/ui/base/button';
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
let { data } = $props();
@@ -24,6 +26,17 @@
<Page.Title>{$t('automation.tabs.mcp')}</Page.Title>
<Page.Subtitle>{$t('automation.mcp.page_subtitle')}</Page.Subtitle>
</Page.HeaderContent>
<Page.Action>
<Button
variant="outline"
href="https://classroomio.com/docs/mcp"
target="_blank"
rel="noopener noreferrer"
>
<ArrowUpRightIcon size={16} />
{$t('automation.mcp.view_docs')}
</Button>
</Page.Action>
</Page.Header>
<Page.Body>
@@ -119,3 +119,33 @@ export const updateOrganizationApiKey = async (
throw new Error('Failed to update organization API key');
}
};
export const rotateOrganizationApiKeyIfActive = async (
organizationId: string,
keyId: string,
data: Pick<TOrganizationApiKey, 'secretPrefix' | 'secretHash'>
): Promise<TOrganizationApiKey | null> => {
try {
const [row] = await db
.update(schema.organizationApiKey)
.set({
secretPrefix: data.secretPrefix,
secretHash: data.secretHash,
lastUsedAt: null,
updatedAt: new Date().toISOString()
})
.where(
and(
eq(schema.organizationApiKey.organizationId, organizationId),
eq(schema.organizationApiKey.id, keyId),
isNull(schema.organizationApiKey.revokedAt)
)
)
.returning();
return row || null;
} catch (error) {
console.error('rotateOrganizationApiKeyIfActive error:', error);
throw new Error('Failed to rotate organization API key');
}
};
-4
View File
@@ -36,7 +36,6 @@ export type TMcpToolName = keyof typeof MCP_TOOL_CREDIT_COST;
export type TAutomationUsageCategory = (typeof AUTOMATION_USAGE_CATEGORY)[keyof typeof AUTOMATION_USAGE_CATEGORY];
export type McpAutomationLimits = {
monthlyCredits: number;
maxActiveKeys: number;
rateLimits: {
perKey: {
@@ -54,7 +53,6 @@ export type McpAutomationLimits = {
export const MCP_AUTOMATION_LIMITS_BY_PLAN: Record<string, McpAutomationLimits> = {
[PLAN.BASIC]: {
monthlyCredits: 20,
maxActiveKeys: 1,
rateLimits: {
perKey: {
@@ -70,7 +68,6 @@ export const MCP_AUTOMATION_LIMITS_BY_PLAN: Record<string, McpAutomationLimits>
}
},
[PLAN.EARLY_ADOPTER]: {
monthlyCredits: 500,
maxActiveKeys: 5,
rateLimits: {
perKey: {
@@ -86,7 +83,6 @@ export const MCP_AUTOMATION_LIMITS_BY_PLAN: Record<string, McpAutomationLimits>
}
},
[PLAN.ENTERPRISE]: {
monthlyCredits: 5000,
maxActiveKeys: 25,
rateLimits: {
perKey: {
+2 -2
View File
@@ -15,7 +15,7 @@ export default {
'20 Students',
'Advanced Course Builder (no video upload)',
'500K AI credits / month',
'MCP access with 20 automation credits / month',
'MCP access',
'ClassroomIO branding'
],
CTA: {
@@ -41,7 +41,7 @@ export default {
'10K Students',
'Course Builder with Video Upload & Certificate',
'3M AI credits / month',
'MCP access with 500 automation credits / month',
'MCP access',
'Custom Branding',
'Custom Domain',
'API with limits',