c28ae7d3c0
- app.md - backend/handlers.go - backend/main.go - frontend/src/App.tsx - frontend/src/api.ts - frontend/vite.config.ts - backend/mcp_http.go - backend/mcp_tokens.go - backend/mcp_tokens_handlers.go - backend/migrations/016_mcp_tokens.sql - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
193 lines
6.0 KiB
TypeScript
193 lines
6.0 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Alert,
|
|
Box,
|
|
Button,
|
|
Code,
|
|
CopyButton,
|
|
Divider,
|
|
Group,
|
|
Loader,
|
|
Modal,
|
|
Stack,
|
|
Table,
|
|
Text,
|
|
TextInput,
|
|
Tooltip,
|
|
} from "@mantine/core";
|
|
import { notifications } from "@mantine/notifications";
|
|
import { IconCopy, IconCheck, IconTrash } from "@tabler/icons-react";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import * as api from "../api";
|
|
import type { MCPToken, MCPTokenCreated } from "../api";
|
|
import { formatDateTimeShort } from "./format";
|
|
|
|
interface Props {
|
|
opened: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function MCPTokensModal({ opened, onClose }: Props) {
|
|
const [tokens, setTokens] = useState<MCPToken[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [newName, setNewName] = useState("");
|
|
const [creating, setCreating] = useState(false);
|
|
const [justCreated, setJustCreated] = useState<MCPTokenCreated | null>(null);
|
|
|
|
const reload = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
setTokens(await api.listMCPTokens());
|
|
} catch (e) {
|
|
notifications.show({ color: "red", message: (e as Error).message });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (opened) {
|
|
reload();
|
|
setJustCreated(null);
|
|
setNewName("");
|
|
}
|
|
}, [opened, reload]);
|
|
|
|
const create = async () => {
|
|
const name = newName.trim() || "default";
|
|
setCreating(true);
|
|
try {
|
|
const t = await api.createMCPToken(name);
|
|
setJustCreated(t);
|
|
setNewName("");
|
|
await reload();
|
|
} catch (e) {
|
|
notifications.show({ color: "red", message: (e as Error).message });
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
const revoke = async (id: string) => {
|
|
if (!confirm("Revocar este token? Quien lo este usando dejara de tener acceso.")) return;
|
|
try {
|
|
await api.revokeMCPToken(id);
|
|
await reload();
|
|
} catch (e) {
|
|
notifications.show({ color: "red", message: (e as Error).message });
|
|
}
|
|
};
|
|
|
|
const mcpURL = `${window.location.origin}/mcp`;
|
|
const claudeCmd = justCreated
|
|
? `claude mcp add kanban --transport http ${mcpURL} --header "Authorization: Bearer ${justCreated.token}"`
|
|
: "";
|
|
|
|
return (
|
|
<Modal opened={opened} onClose={onClose} title="MCP Tokens" size="lg">
|
|
<Stack gap="md">
|
|
<Text size="sm" c="dimmed">
|
|
Cada token deja conectar un cliente Claude al kanban como tu usuario.
|
|
El valor solo aparece UNA vez al crearlo. Si lo pierdes, generas otro y revocas el antiguo.
|
|
</Text>
|
|
|
|
<Group align="end">
|
|
<TextInput
|
|
label="Nombre del token"
|
|
placeholder="ej. portatil, sobremesa..."
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.currentTarget.value)}
|
|
style={{ flex: 1 }}
|
|
disabled={creating}
|
|
/>
|
|
<Button onClick={create} loading={creating}>
|
|
Generar
|
|
</Button>
|
|
</Group>
|
|
|
|
{justCreated && (
|
|
<Alert color="yellow" title="Copia el token ahora — no se mostrara mas">
|
|
<Stack gap="xs">
|
|
<Group gap="xs" align="center">
|
|
<Code style={{ flex: 1, wordBreak: "break-all" }}>{justCreated.token}</Code>
|
|
<CopyButton value={justCreated.token}>
|
|
{({ copied, copy }) => (
|
|
<Tooltip label={copied ? "Copiado" : "Copiar token"}>
|
|
<ActionIcon variant="subtle" onClick={copy}>
|
|
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
)}
|
|
</CopyButton>
|
|
</Group>
|
|
<Divider />
|
|
<Text size="xs" c="dimmed">
|
|
Pega este comando en tu PC para registrar el MCP en Claude Code:
|
|
</Text>
|
|
<Group gap="xs" align="center">
|
|
<Code block style={{ flex: 1 }}>{claudeCmd}</Code>
|
|
<CopyButton value={claudeCmd}>
|
|
{({ copied, copy }) => (
|
|
<Tooltip label={copied ? "Copiado" : "Copiar comando"}>
|
|
<ActionIcon variant="subtle" onClick={copy}>
|
|
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
)}
|
|
</CopyButton>
|
|
</Group>
|
|
</Stack>
|
|
</Alert>
|
|
)}
|
|
|
|
<Divider label="Tokens activos" labelPosition="left" />
|
|
|
|
{loading ? (
|
|
<Group justify="center" p="md">
|
|
<Loader size="sm" />
|
|
</Group>
|
|
) : tokens.length === 0 ? (
|
|
<Text size="sm" c="dimmed" ta="center" py="md">
|
|
Sin tokens. Genera uno arriba.
|
|
</Text>
|
|
) : (
|
|
<Table withTableBorder withColumnBorders verticalSpacing="xs" highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Nombre</Table.Th>
|
|
<Table.Th>Creado</Table.Th>
|
|
<Table.Th>Ultimo uso</Table.Th>
|
|
<Table.Th w={60} />
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{tokens.map((t) => (
|
|
<Table.Tr key={t.id}>
|
|
<Table.Td>{t.name}</Table.Td>
|
|
<Table.Td>{formatDateTimeShort(t.created_at)}</Table.Td>
|
|
<Table.Td>
|
|
{t.last_used_at ? formatDateTimeShort(t.last_used_at) : <Text c="dimmed">nunca</Text>}
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Tooltip label="Revocar">
|
|
<ActionIcon color="red" variant="subtle" onClick={() => revoke(t.id)}>
|
|
<IconTrash size={14} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
)}
|
|
|
|
<Box>
|
|
<Text size="xs" c="dimmed">
|
|
Endpoint MCP: <Code>{mcpURL}</Code>
|
|
</Text>
|
|
</Box>
|
|
</Stack>
|
|
</Modal>
|
|
);
|
|
}
|