Improve MCP tooling and draft exercise publishing

This commit is contained in:
Rotimi Best
2026-03-21 10:04:39 +01:00
parent 345055421c
commit e9c4082f89
21 changed files with 711 additions and 72 deletions
@@ -63,6 +63,17 @@ class AutomationApi extends BaseApiWithErrors {
this.usage = { ...this.usage, activeKeys: this.usage.activeKeys + 1 };
}
snackbar.success('snackbar.automation.created');
},
onError: (result) => {
if (typeof result === 'string') {
return;
}
this.handleValidationError(result);
if ('field' in result && result.field) {
this.errors[result.field] = result.error;
}
}
});
}
@@ -4,17 +4,21 @@
getClaudeCodeSnippet,
getCodexSnippet,
getCursorSnippet,
getOpenCodeSnippet,
getMaskedAutomationSecret
} from '$features/automation/utils/automation-utils';
import { isOrgAdmin } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
import * as Alert from '@cio/ui/base/alert';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import * as Code from '@cio/ui/custom/code';
import * as Dialog from '@cio/ui/base/dialog';
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import * as Field from '@cio/ui/base/field';
import * as Table from '@cio/ui/base/table';
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';
@@ -27,11 +31,29 @@
let activeSetupTab = $state('cursor');
let generatedSecret = $state<string | null>(null);
let isCreateKeyModalOpen = $state(false);
let keyLabel = $state('');
const canCreateKey = $derived(keyLabel.trim().length > 0 && !automationApi.isLoading);
function resetCreateKeyModal() {
keyLabel = '';
automationApi.resetErrors();
}
function openCreateKeyModal() {
resetCreateKeyModal();
isCreateKeyModalOpen = true;
}
async function onGenerateKey() {
await automationApi.createKey('mcp');
const result = await automationApi.createKey('mcp', keyLabel);
if (!result) return;
generatedSecret = automationApi.generatedSecret;
automationApi.clearGeneratedSecret();
isCreateKeyModalOpen = false;
resetCreateKeyModal();
}
async function onRevokeKey(keyId: string) {
@@ -108,7 +130,7 @@
</div>
{#if $isOrgAdmin}
<IconButton
onclick={onGenerateKey}
onclick={openCreateKeyModal}
disabled={automationApi.isLoading}
tooltip={$t('automation.mcp.keys.generate')}
>
@@ -209,6 +231,7 @@
<Tabs.Trigger value="cursor">{$t('automation.clients.cursor')}</Tabs.Trigger>
<Tabs.Trigger value="claude-code">{$t('automation.clients.claude_code')}</Tabs.Trigger>
<Tabs.Trigger value="codex">{$t('automation.clients.codex')}</Tabs.Trigger>
<Tabs.Trigger value="opencode">{$t('automation.clients.opencode')}</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="claude-code" class="mt-4">
@@ -234,6 +257,49 @@
</Code.Root>
</Code.Overflow>
</Tabs.Content>
<Tabs.Content value="opencode" class="mt-4">
<Code.Overflow>
<Code.Root code={getOpenCodeSnippet(generatedSecret)} lang="json">
<Code.CopyButton />
</Code.Root>
</Code.Overflow>
</Tabs.Content>
</Tabs.Root>
</Field.Set>
</Field.Group>
<Dialog.Root
bind:open={isCreateKeyModalOpen}
onOpenChange={(isOpen) => {
if (!isOpen) {
resetCreateKeyModal();
}
}}
>
<Dialog.Content class="sm:max-w-md">
<Dialog.Header>
<Dialog.Title>{$t('automation.mcp.keys.modal.title')}</Dialog.Title>
<Dialog.Description>{$t('automation.mcp.keys.modal.description')}</Dialog.Description>
</Dialog.Header>
<div class="py-2">
<InputField
label={$t('automation.mcp.keys.modal.name_label')}
placeholder={$t('automation.mcp.keys.modal.name_placeholder')}
bind:value={keyLabel}
errorMessage={automationApi.errors.label}
autoFocus
/>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => (isCreateKeyModalOpen = false)}>
{$t('automation.mcp.keys.modal.cancel')}
</Button>
<Button onclick={onGenerateKey} loading={automationApi.isLoading} disabled={!canCreateKey}>
{$t('automation.mcp.keys.modal.save')}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
@@ -30,6 +30,25 @@ export function getCodexSnippet(secret: string | null) {
-- npx -y @classroomio/mcp`;
}
export function getOpenCodeSnippet(secret: string | null) {
const apiKey = getAutomationSetupSecret(secret);
return `{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"classroomio": {
"type": "local",
"command": ["npx", "-y", "@classroomio/mcp"],
"enabled": true,
"environment": {
"CLASSROOMIO_API_URL": "https://api.classroomio.com",
"CLASSROOMIO_API_KEY": "${apiKey}"
}
}
}
}`;
}
export function getCursorSnippet(secret: string | null) {
const apiKey = getAutomationSetupSecret(secret);