Files
MindSift/frontend/src/features/settings/ApiKeysPanel.tsx
T

650 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from 'react';
import {
Key,
Plus,
Trash2,
Pencil,
Copy,
Check,
Webhook,
Zap,
Terminal,
Bot,
} from 'lucide-react';
import { APIKey, ApiKeyScope, PriorityLevel, SharedList, TaskList } from '../../lib/types';
import { SCOPES, SCOPE_GROUPS, scopeChip } from '../../lib/apiScopes';
import { api, triggerHaptic } from '../../lib/api';
import { ToggleSwitch } from '../../components/ToggleSwitch';
interface ApiKeysPanelProps {
onIngested?: () => void;
}
type SimTarget = 'triage' | 'task' | 'list_item';
type McpClient = 'connector' | 'claude' | 'codex' | 'json';
type Access = 'rest' | 'mcp';
// Personal API keys with rights (switches) and how to connect: curl / smart home (read examples,
// delivery simulator) or AI agents over MCP (settings → API)
export const ApiKeysPanel: React.FC<ApiKeysPanelProps> = ({ onIngested }) => {
const [keys, setKeys] = useState<APIKey[]>([]);
const [newKeyName, setNewKeyName] = useState('');
const [newKeyScopes, setNewKeyScopes] = useState<ApiKeyScope[]>(['webhook:triage']);
const [newlyCreatedKey, setNewlyCreatedKey] = useState<string | null>(null);
const [copiedKey, setCopiedKey] = useState(false);
const [mcpClient, setMcpClient] = useState<McpClient>('connector');
// Address for claude.ai / ChatGPT connectors; empty while the server has no OAuth (no BASE_URL)
const [connectorUrl, setConnectorUrl] = useState('');
const [copiedConnector, setCopiedConnector] = useState(false);
const [access, setAccess] = useState<Access>('rest');
// The operator can switch MCP off; then only the curl / smart home access is shown
const [mcpEnabled, setMcpEnabled] = useState(true);
// Live Webhook Simulator State
const [simTitle, setSimTitle] = useState('Waschmaschine im Keller ist fertig!');
const [simPriority, setSimPriority] = useState<PriorityLevel>('high');
const [simTarget, setSimTarget] = useState<SimTarget>('triage');
const [simListId, setSimListId] = useState('');
const [simTaskListId, setSimTaskListId] = useState('');
const [simStatus, setSimStatus] = useState<string | null>(null);
const [lists, setLists] = useState<SharedList[]>([]);
const [taskLists, setTaskLists] = useState<TaskList[]>([]);
const loadKeys = async () => {
const k = await api.getAPIKeys();
setKeys(k);
};
useEffect(() => {
loadKeys();
api.getAllLists().then(setLists).catch(() => {});
api.getTaskLists().then(setTaskLists).catch(() => {});
api
.getAuthStatus()
.then((st) => {
setConnectorUrl(st.mcpConnectorUrl || '');
setMcpEnabled(st.mcpEnabled !== false);
})
.catch(() => {});
}, []);
// Editing an existing key: name and rights (the secret stays the same)
const [editKeyId, setEditKeyId] = useState<string | null>(null);
const [editName, setEditName] = useState('');
const [editScopes, setEditScopes] = useState<ApiKeyScope[]>([]);
const [editError, setEditError] = useState<string | null>(null);
const startEdit = (k: APIKey) => {
setEditKeyId(k.id);
setEditName(k.name);
setEditScopes(k.scopes.split(',').filter(Boolean) as ApiKeyScope[]);
setEditError(null);
};
const toggleEditScope = (id: ApiKeyScope) => {
setEditScopes((prev) => {
const next = prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id];
return next.length > 0 ? next : prev; // at least one
});
};
const handleSaveEdit = async () => {
if (!editKeyId) return;
try {
await api.updateAPIKey(editKeyId, { name: editName.trim(), scopes: editScopes });
triggerHaptic(15);
setEditKeyId(null);
await loadKeys();
} catch (err: any) {
setEditError(err?.message || 'Speichern fehlgeschlagen');
}
};
// Switches grouped by tasks, lists, notes and inbox (same layout for a new and an existing key)
const renderScopeSwitches = (selected: ApiKeyScope[], toggle: (id: ApiKeyScope) => void, label: string) => (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3" role="group" aria-label={label}>
{SCOPE_GROUPS.filter(([group]) => mcpEnabled || group !== 'notes').map(([group, heading]) => (
<div key={group} className="border border-border rounded-card bg-panel px-3 py-1.5">
<p className="text-[11px] text-muted pt-1 pb-0.5 font-semibold">{heading}</p>
{SCOPES.filter((sc) => sc.group === group).map((sc) => (
<div key={sc.id} className="flex items-center justify-between gap-3 min-h-[44px] sm:min-h-[36px]">
<div className="min-w-0">
<span className="block text-xs text-main">{sc.label}</span>
<span className="block text-[11px] text-muted">{sc.hint}</span>
</div>
<ToggleSwitch checked={selected.includes(sc.id)} onChange={() => toggle(sc.id)} title={sc.chip} />
</div>
))}
</div>
))}
</div>
);
const toggleScope = (id: ApiKeyScope) => {
setNewKeyScopes((prev) => {
const next = prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id];
return next.length > 0 ? next : prev; // at least one
});
};
const handleCreateKey = async (e: React.FormEvent) => {
e.preventDefault();
triggerHaptic(15);
const created = await api.createAPIKey(newKeyName.trim() || 'Home Assistant', newKeyScopes);
if (created.rawKey) {
setNewlyCreatedKey(created.rawKey);
}
setNewKeyName('');
setNewKeyScopes(['webhook:triage']);
await loadKeys();
};
const handleDeleteKey = async (k: APIKey) => {
if (!window.confirm(k.kind === 'oauth' ? `Die Verbindung „${k.name}“ wirklich trennen?` : 'Diesen API-Schlüssel wirklich widerrufen?')) return;
triggerHaptic(20);
await api.deleteAPIKey(k.id);
await loadKeys();
};
const handleCopy = (text: string) => {
navigator.clipboard.writeText(text);
setCopiedKey(true);
setTimeout(() => setCopiedKey(false), 2000);
};
const handleSimulateWebhook = async () => {
triggerHaptic(20);
if (simTarget === 'list_item' && !simListId) {
setSimStatus('❌ Bitte eine Liste wählen.');
return;
}
try {
// With a freshly created key the simulator tests exactly that key (and its scopes)
await api.testWebhookIngest(newlyCreatedKey, {
title: simTitle,
priority: simPriority,
tags: simTarget === 'list_item' ? [] : ['smarthome', 'automation'],
target: simTarget,
taskListId: simTarget === 'task' ? simTaskListId || undefined : undefined,
listId: simTarget === 'list_item' ? simListId : undefined,
});
setSimStatus(
simTarget === 'task'
? '✅ Webhook erfolgreich ausgelöst! Die Aufgabe ist angelegt.'
: simTarget === 'list_item'
? '✅ Webhook erfolgreich ausgelöst! Der Eintrag steht in der Liste.'
: '✅ Webhook erfolgreich ausgelöst! Neue Karte liegt in deiner Inbox.'
);
onIngested?.();
setTimeout(() => setSimStatus(null), 4000);
} catch (err: any) {
setSimStatus(`❌ Fehler: ${err.message}`);
}
};
const exampleToken = newlyCreatedKey || 'mindsift_sec_dein_api_key';
const baseUrl = window.location.origin;
const examplePayload =
simTarget === 'task'
? `{
"target": "task",
"title": "Filter der Lüftungsanlage wechseln",
"priority": "high",
"dueDate": "2026-11-15T09:00"${simTaskListId ? `,\n "taskListId": "${simTaskListId}"` : ''}
}`
: simTarget === 'list_item'
? `{
"target": "list_item",
"listId": "${simListId || 'ID_DER_LISTE'}",
"content": "Hafermilch",
"quantity": "2"
}`
: `{
"title": "Kellerfeuchtigkeit über 70%",
"priority": "urgent",
"tags": ["smarthome", "keller"]
}`;
const mcpUrl = `${baseUrl}/api/v1/mcp`;
const mcpSnippet =
mcpClient === 'claude'
? `claude mcp add --transport http mindsift ${mcpUrl} \\
--header "X-API-Key: ${exampleToken}"`
: mcpClient === 'codex'
? `# ~/.codex/config.toml
[mcp_servers.mindsift]
url = "${mcpUrl}"
http_headers = { "X-API-Key" = "${exampleToken}" }`
: `{
"mcpServers": {
"mindsift": {
"type": "http",
"url": "${mcpUrl}",
"headers": { "X-API-Key": "${exampleToken}" }
}
}
}`;
return (
<div className="space-y-6">
{/* API keys */}
<div>
<div className="flex items-center gap-3 mb-2">
<div className="w-8 h-8 bg-accent text-on-accent flex items-center justify-center flex-shrink-0">
<Key className="w-4 h-4" />
</div>
<div>
<h3 className="text-sm font-bold text-main">API-Schlüssel</h3>
<p className="text-xs text-muted">
Ein Schlüssel verbindet Home Assistant, n8n, eigene Skripte oder KI-Agenten (Claude Code, Codex …) mit
MindSift. Was er darf, legst du mit den Schaltern fest; das gilt per curl und per MCP gleich, nur Notizen
gibt es ausschließlich über MCP. Einstellungen bleiben für Schlüssel gesperrt. Was ein Schlüssel bei Aufgaben
und Listen anlegt oder ändert, trägt seinen Namen als Badge.
</p>
</div>
</div>
<form onSubmit={handleCreateKey} className="mt-3 space-y-2">
<div className="flex flex-col sm:flex-row gap-2">
<input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="Bezeichnung (z. B. Home Assistant, Proxmox Backup, Rauchmelder)…"
className="w-full sm:flex-1 min-w-0 h-11 sm:h-9 px-3 input-field text-base sm:text-sm"
/>
<button type="submit" className="btn-primary h-11 sm:h-9 px-4 text-sm flex items-center justify-center gap-1.5">
<Plus className="w-4 h-4" />
<span>API-Key erzeugen</span>
</button>
</div>
{renderScopeSwitches(newKeyScopes, toggleScope, 'Berechtigungen des Schlüssels')}
</form>
{newlyCreatedKey && (
<div className="mt-3 p-3 bg-accent/15 border border-accent/30 flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-semibold text-accent mb-1">Neuer API-Schlüssel erstellt (bitte jetzt kopieren):</p>
<code className="text-xs sm:text-sm font-mono text-main break-all">{newlyCreatedKey}</code>
</div>
<button onClick={() => handleCopy(newlyCreatedKey)} className="btn-primary h-8 px-3 text-xs">
{copiedKey ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
<span>{copiedKey ? 'Kopiert!' : 'Key kopieren'}</span>
</button>
</div>
)}
{/* Active Keys List */}
<div className="mt-3 border border-border divide-y divide-border">
{keys.length === 0 ? (
<p className="text-xs text-faint px-3 py-2">Noch keine dedizierten API-Schlüssel hinterlegt.</p>
) : (
keys.map((k) =>
editKeyId === k.id ? (
<div key={k.id} className="px-3 py-3 bg-canvas space-y-2">
<div className="flex flex-col sm:flex-row gap-2">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
aria-label="Name des Schlüssels"
className="w-full sm:flex-1 min-w-0 h-11 sm:h-9 px-3 input-field text-base sm:text-sm"
/>
<div className="flex gap-2">
<button type="button" onClick={() => setEditKeyId(null)} className="btn-action h-11 sm:h-9 px-3 text-xs flex-1 sm:flex-none">
Abbrechen
</button>
<button
type="button"
onClick={handleSaveEdit}
disabled={!editName.trim()}
className="btn-primary h-11 sm:h-9 px-4 text-xs flex-1 sm:flex-none disabled:opacity-40"
>
Speichern
</button>
</div>
</div>
{renderScopeSwitches(editScopes, toggleEditScope, `Berechtigungen von ${k.name}`)}
<p className="text-[11px] text-faint">
{k.kind === 'oauth'
? 'Die Verbindung bleibt bestehen; die neuen Rechte gelten ab sofort.'
: 'Der Schlüssel selbst bleibt gleich; Geräte und Agenten, die ihn nutzen, müssen nichts ändern. Die neuen Rechte gelten ab sofort.'}
</p>
{editError && <p className="text-[11px] text-status-err">{editError}</p>}
</div>
) : (
<div key={k.id} className="px-3 py-2 bg-canvas flex items-center justify-between gap-3">
<div className="min-w-0 flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="text-sm font-medium text-main">{k.name}</span>
{k.kind === 'oauth' ? (
<span className="px-1.5 py-0.5 rounded-btn bg-accent/15 text-accent text-[10px] font-semibold" title="Verbunden über OAuth (claude.ai, ChatGPT …)">
OAuth
</span>
) : (
<span className="text-xs font-mono text-accent">{k.keyPrefix}</span>
)}
{k.scopes
.split(',')
.filter(Boolean)
.map((sc) => (
<span key={sc} className="px-1.5 py-0.5 rounded-btn border border-border text-muted text-[10px]">
{scopeChip(sc)}
</span>
))}
{k.lastUsedAt && (
<span className="text-xs text-faint">
Zuletzt genutzt: {new Date(k.lastUsedAt).toLocaleDateString('de-DE')}
</span>
)}
</div>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={() => startEdit(k)}
className="btn-icon w-7 h-7"
aria-label={`API-Key ${k.name} bearbeiten`}
title="Name und Rechte ändern"
>
<Pencil className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleDeleteKey(k)}
className="btn-icon btn-icon-danger w-7 h-7"
aria-label={`API-Key ${k.name} löschen`}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
)
)
)}
</div>
</div>
{/* How to connect: one API, two accesses */}
<div className="pt-4 border-t border-border space-y-4">
{mcpEnabled && (
<div className="flex flex-wrap gap-1.5" role="tablist" aria-label="Zugang">
{(
[
['rest', 'curl / Smart Home', Terminal],
['mcp', 'KI-Agenten (MCP)', Bot],
] as const
).map(([value, label, Icon]) => (
<button
key={value}
type="button"
role="tab"
aria-selected={access === value}
onClick={() => setAccess(value)}
className={`h-11 sm:h-8 px-3 rounded-btn border text-xs flex items-center gap-1.5 transition-colors ${
access === value ? 'border-accent bg-accent/10 text-main' : 'border-border text-muted hover:text-main'
}`}
>
<Icon className="w-3.5 h-3.5 text-accent" />
{label}
</button>
))}
</div>
)}
{(access === 'rest' || !mcpEnabled) && (
<div className="space-y-5">
{/* Reading: e.g. a smart home sensor for what is due today */}
<div>
<div className="flex items-center gap-2 text-xs font-medium text-muted mb-2">
<Terminal className="w-3.5 h-3.5 text-accent" />
<span>Lesen, z. B. was heute fällig ist (Recht „Aufgaben lesen“):</span>
</div>
<pre className="p-3 bg-canvas border border-border font-mono text-xs text-main overflow-x-auto">
{`curl "${baseUrl}/api/v1/tasks?due=today&status=open" \\
-H "X-API-Key: ${exampleToken}"`}
</pre>
<p className="mt-2 text-[11px] text-faint">
Filter: <code className="font-mono">due=today</code>, <code className="font-mono">due=overdue</code> oder ein
Datum (<code className="font-mono">JJJJ-MM-TT</code>, fällig bis einschließlich), <code className="font-mono">status=open|done</code>,{' '}
<code className="font-mono">taskListId</code>. Listen: <code className="font-mono">GET /api/v1/lists</code> und{' '}
<code className="font-mono">/lists/ID</code>. Mit „Ändern“ auch <code className="font-mono">PATCH /api/v1/thoughts/ID</code>{' '}
(z. B. <code className="font-mono">{'{"isCompleted": true}'}</code>).
</p>
<div className="flex items-center gap-2 text-xs font-medium text-muted mt-3 mb-2">
<Terminal className="w-3.5 h-3.5 text-accent" />
<span>Home Assistant: Sensor „Heute fällig“ (configuration.yaml)</span>
</div>
<pre className="p-3 bg-canvas border border-border font-mono text-xs text-main overflow-x-auto">
{`rest:
- resource: "${baseUrl}/api/v1/tasks?due=today&status=open"
headers:
X-API-Key: "${exampleToken}"
scan_interval: 900
sensor:
- name: "MindSift heute fällig"
value_template: "{{ value_json | length }}"`}
</pre>
</div>
<div className="pt-4 border-t border-border">
<div className="flex items-center gap-3 mb-3">
<div className="w-7 h-7 bg-accent text-on-accent flex items-center justify-center flex-shrink-0">
<Webhook className="w-4 h-4" />
</div>
<div>
<h3 className="text-sm font-bold text-main">Einliefern testen (Webhook)</h3>
<p className="text-xs text-muted">
Teste das Einliefern per <code className="font-mono">POST /api/v1/webhooks/ingest</code> direkt hier
{newlyCreatedKey ? ' – mit dem eben erzeugten Key und seinen Berechtigungen.' : ' – mit deiner Anmeldung.'}
</p>
</div>
</div>
<div className="flex flex-wrap gap-1.5 mb-3" role="radiogroup" aria-label="Ziel">
{(
[
['triage', '📥 Inbox'],
['task', '📋 Aufgabe'],
['list_item', '📝 Listeneintrag'],
] as const
).map(([value, label]) => (
<button
key={value}
type="button"
role="radio"
aria-checked={simTarget === value}
onClick={() => setSimTarget(value)}
className={`h-11 sm:h-8 px-3 rounded-btn border text-xs transition-colors ${
simTarget === value ? 'border-accent bg-accent/10 text-main' : 'border-border text-muted hover:text-main'
}`}
>
{label}
</button>
))}
{simTarget === 'list_item' && (
<select
value={simListId}
onChange={(e) => setSimListId(e.target.value)}
className="h-11 sm:h-8 px-2 input-field text-base sm:text-xs bg-panel"
aria-label="Liste"
>
<option value="">Liste wählen…</option>
{lists.map((l) => (
<option key={l.id} value={l.id}>
{l.icon} {l.title}
</option>
))}
</select>
)}
{simTarget === 'task' && (
<select
value={simTaskListId}
onChange={(e) => setSimTaskListId(e.target.value)}
className="h-11 sm:h-8 px-2 input-field text-base sm:text-xs bg-panel"
aria-label="Aufgabenliste"
>
<option value="">Ohne Aufgabenliste</option>
{taskLists.map((tl) => (
<option key={tl.id} value={tl.id}>
{tl.icon} {tl.title}
</option>
))}
</select>
)}
</div>
{simTarget === 'triage' && (
<div className="flex flex-wrap gap-1.5 mb-3">
{[
{ label: '🧺 Waschmaschine fertig', text: 'Waschmaschine im Keller ist fertig – Wäsche aufhängen!', prio: 'high' as PriorityLevel },
{ label: '🚨 Rauchmelder Batterie', text: 'Rauchmelder Dachgeschoss meldet niedrige Batterie (12%)', prio: 'urgent' as PriorityLevel },
{ label: '💧 Luftfeuchtigkeit Keller', text: 'Kellerfeuchtigkeit über 70% – Lüften oder Entfeuchter starten', prio: 'urgent' as PriorityLevel },
].map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => {
setSimTitle(preset.text);
setSimPriority(preset.prio);
}}
className="btn-action text-xs"
>
{preset.label}
</button>
))}
</div>
)}
<div className="flex flex-col sm:flex-row gap-2">
<input
type="text"
value={simTitle}
onChange={(e) => setSimTitle(e.target.value)}
className="flex-1 h-11 sm:h-9 px-3 input-field text-base sm:text-sm"
/>
<button
type="button"
onClick={handleSimulateWebhook}
className="btn-primary h-11 sm:h-9 px-4 text-xs sm:text-sm flex items-center justify-center gap-1.5"
>
<Zap className="w-4 h-4" />
<span>Webhook jetzt abfeuern</span>
</button>
</div>
{simStatus && (
<p className={`mt-3 text-xs font-medium ${simStatus.startsWith('❌') ? 'text-status-err' : 'text-status-ok'}`}>{simStatus}</p>
)}
{/* Code Example Snippets */}
<div className="mt-4 space-y-2">
<div className="flex items-center gap-2 text-xs font-medium text-muted">
<Terminal className="w-3.5 h-3.5 text-accent" />
<span>cURL / Home Assistant REST Command:</span>
</div>
<pre className="p-3 bg-canvas border border-border font-mono text-xs text-main overflow-x-auto">
{`curl -X POST "${baseUrl}/api/v1/webhooks/ingest" \\
-H "X-API-Key: ${exampleToken}" \\
-H "Content-Type: application/json" \\
-d '${examplePayload}'`}
</pre>
<p className="text-[11px] text-faint">
Der Key gehört in den Header <code className="font-mono">X-API-Key</code>, nicht in die URL, damit er nicht in
Proxy-Logs landet. Fehlt dem Key die Berechtigung für das Ziel, antwortet der Server mit 403.
</p>
</div>
</div>
</div>
)}
{access === 'mcp' && mcpEnabled && (
<div>
<div className="flex items-center gap-3 mb-3">
<div className="w-7 h-7 bg-accent text-on-accent flex items-center justify-center flex-shrink-0">
<Bot className="w-4 h-4" />
</div>
<div>
<h3 className="text-sm font-bold text-main">KI-Agenten anbinden (MCP)</h3>
<p className="text-xs text-muted">
MindSift ist ein MCP-Server: Claude Code, Codex, Claude Desktop, Cursor und eigene Agenten nutzen dieselben
Schlüssel und Rechte wie curl, nur in der Form, die Agenten erwarten. claude.ai und ChatGPT verbinden sich ohne
Schlüssel: MindSift fragt beim Verbinden, was der Agent darf.
</p>
</div>
</div>
<div className="flex flex-wrap gap-1.5 mb-2" role="radiogroup" aria-label="Agent">
{(
[
['connector', 'claude.ai / ChatGPT'],
['claude', 'Claude Code'],
['codex', 'OpenAI Codex'],
['json', 'Claude Desktop / Cursor'],
] as const
).map(([value, label]) => (
<button
key={value}
type="button"
role="radio"
aria-checked={mcpClient === value}
onClick={() => setMcpClient(value)}
className={`h-11 sm:h-8 px-3 rounded-btn border text-xs transition-colors ${
mcpClient === value ? 'border-accent bg-accent/10 text-main' : 'border-border text-muted hover:text-main'
}`}
>
{label}
</button>
))}
</div>
{mcpClient === 'connector' ? (
connectorUrl ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<code className="flex-1 min-w-0 p-3 bg-canvas border border-border font-mono text-xs text-main break-all">{connectorUrl}</code>
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(connectorUrl);
setCopiedConnector(true);
setTimeout(() => setCopiedConnector(false), 2000);
}}
className="btn-icon w-11 h-11 sm:w-9 sm:h-9 flex-shrink-0"
aria-label="Adresse kopieren"
title="Adresse kopieren"
>
{copiedConnector ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</button>
</div>
<p className="text-xs text-muted">
In claude.ai unter <span className="text-main">Einstellungen → Connectors → Eigenen Connector hinzufügen</span>{' '}
diese Adresse eintragen (in ChatGPT entsprechend als Connector bzw. App). Beim Verbinden öffnet sich MindSift:
anmelden, Rechte wählen, erlauben. Die Verbindung erscheint oben in der Liste und lässt sich dort ändern oder
trennen; unbenutzt endet sie nach einiger Zeit von selbst.
</p>
</div>
) : (
<p className="p-3 bg-canvas border border-border text-xs text-muted">
Verbinden über claude.ai und ChatGPT braucht OAuth, und das setzt <code className="font-mono">BASE_URL</code> in
der Server-Konfiguration voraus. Bitte die Person, die MindSift betreibt, sie zu setzen.
</p>
)
) : (
<pre className="p-3 bg-canvas border border-border font-mono text-xs text-main overflow-x-auto">{mcpSnippet}</pre>
)}
<p className="mt-2 text-[11px] text-faint">
Werkzeuge: <code className="font-mono">list_task_lists</code>, <code className="font-mono">create_task_list</code>,{' '}
<code className="font-mono">list_tasks</code>, <code className="font-mono">get_task</code>,{' '}
<code className="font-mono">create_task</code>, <code className="font-mono">update_task</code>,{' '}
<code className="font-mono">complete_task</code>, <code className="font-mono">delete_task</code>,{' '}
<code className="font-mono">list_lists</code>, <code className="font-mono">create_list</code>,{' '}
<code className="font-mono">get_list</code>, <code className="font-mono">add_list_item</code>,{' '}
<code className="font-mono">check_list_item</code>, <code className="font-mono">update_list_item</code>,{' '}
<code className="font-mono">list_notes</code>, <code className="font-mono">get_note</code>,{' '}
<code className="font-mono">create_note</code>, <code className="font-mono">search</code>,{' '}
<code className="font-mono">capture_to_inbox</code>. Der Agent sieht
nur die Werkzeuge, für die der Schlüssel bzw. die Verbindung berechtigt ist; Freigaben gelten wie für dich selbst.
</p>
</div>
)}
</div>
</div>
);
};