feat(mcp): MCP server for AI agents, agent scopes on API keys, switches in settings
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m19s

MindSift now speaks MCP (Streamable HTTP, stateless JSON-RPC) at POST /api/v1/mcp so
Claude Code, Codex, Claude Desktop, Cursor and custom agents can read, create and
complete tasks and keep shared lists. API keys get the scopes tasks:read/write and
lists:read/write; tools/list only shows what the key may call and every tool checks
its scope. Task and list write paths are shared helpers (updateTask, createTask,
addListItem, setListItemDone) used by REST and MCP alike.

Settings: scope selection and the SectionPicker (import, restore) use ToggleSwitch
rows instead of checkbox-like buttons; new MCP section with connection snippets for
Claude Code, Codex and JSON configs. Docs, README, help and changelog updated; v0.28.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Fable 5.1 committed 2026-10-31 21:58:44 +01:00
1 parent 5982563579
commit 8faed7a160
22 files changed
+1243 -147

No files matched your search

+12 -4
View File
@@ -492,8 +492,8 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
{
id: 'triage-webhooks',
category: 'triage',
title: 'Externe Erfassung per Webhooks & API',
subtitle: 'Gedanken von Siri, Home Assistant, n8n oder curl einspeisen',
title: 'Webhooks, API & KI-Agenten (MCP)',
subtitle: 'Gedanken von Siri, Home Assistant, n8n oder curl einspeisen; Aufgaben und Listen für Claude Code, Codex & Co.',
icon: Bell,
keywords: [
'webhooks',
@@ -504,11 +504,19 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
'shortcuts',
'automation',
'curl',
'mcp',
'ki',
'agent',
'claude',
'codex',
],
content: (
<div className="space-y-2 text-muted leading-relaxed">
<p>
Unter <em className="text-main">Kontoeinstellungen → Private Webhooks &amp; Export</em> kannst du persönliche API-Schlüssel erzeugen. Pro Schlüssel legst du fest, was er einliefern darf: Gedanken in die Inbox, Aufgaben direkt oder Einträge in Listen. Lesen kann ein Schlüssel nie.
Unter <em className="text-main">Kontoeinstellungen → Webhooks, KI-Agenten &amp; Export</em> kannst du persönliche API-Schlüssel erzeugen. Pro Schlüssel legst du mit Schaltern fest, was er darf: Gedanken in die Inbox, Aufgaben oder Listeneinträge einliefern (Webhooks) oder für KI-Agenten Aufgaben und Listen lesen und ändern. Notizen erreicht ein Schlüssel nie.
</p>
<p>
<strong className="text-main">KI-Agenten:</strong> MindSift ist ein MCP-Server (<code className="text-accent bg-panel px-1 py-0.5 rounded font-mono">POST /api/v1/mcp</code>). Claude Code, OpenAI Codex, Claude Desktop, Cursor und eigene Agenten können damit offene Aufgaben auslesen, abhaken und anlegen sowie Einkaufs- und Checklisten führen. Die Einstellungen zeigen den fertigen Befehl bzw. die Konfiguration für den jeweiligen Agenten; der Agent sieht nur die Werkzeuge, für die der Schlüssel freigegeben ist.
</p>
<p>
Mit dem Feld <code className="text-accent bg-panel px-1 py-0.5 rounded font-mono">"target"</code> wählst du das Ziel: <code className="font-mono">triage</code> (Standard), <code className="font-mono">task</code> (optional mit <code className="font-mono">taskListId</code>) oder <code className="font-mono">list_item</code> (mit <code className="font-mono">listId</code>). Der Simulator in den Einstellungen zeigt passende Beispiele.
@@ -656,7 +664,7 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
<strong className="text-main">Eigenes Theme:</strong> Vorlage herunterladen oder einfügen, Farben und Rundungen anpassen, als <code className="text-accent bg-panel px-1 py-0.5 rounded font-mono">.css</code>-Datei hochladen oder einfügen und „Speichern &amp; aktivieren“. Das Theme greift sofort und steht dir auf allen Geräten zur Verfügung.
</li>
<li>
<strong className="text-main">Server-Backups:</strong> Hat die Administration automatische Backups eingeschaltet, findest du unter <em>Kontoeinstellungen → Private Webhooks &amp; Export</em> die täglichen Sicherungen deiner Daten. Du kannst sie herunterladen oder wieder einspielen, auch nur für einzelne Bereiche wie Listen oder Aufgaben.
<strong className="text-main">Server-Backups:</strong> Hat die Administration automatische Backups eingeschaltet, findest du unter <em>Kontoeinstellungen → Webhooks, KI-Agenten &amp; Export</em> die täglichen Sicherungen deiner Daten. Du kannst sie herunterladen oder wieder einspielen, auch nur für einzelne Bereiche wie Listen oder Aufgaben.
</li>
<li>
<strong className="text-main">Ohne Internet:</strong> Als App installiert (oder im Browser) startet MindSift auch offline mit dem zuletzt geladenen Stand. Gedanken erfassen sowie Aufgaben und Listeneinträge abhaken geht weiter; ein Hinweis unten zeigt, wie viele Änderungen warten, und sie werden automatisch übertragen, sobald die Verbindung zurück ist.
+121 -31
View File
@@ -8,31 +8,41 @@ import {
Webhook,
Zap,
Terminal,
Bot,
} from 'lucide-react';
import { APIKey, ApiKeyScope, PriorityLevel, SharedList, TaskList } from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api';
import { ToggleSwitch } from '../../components/ToggleSwitch';
interface ApiKeysPanelProps {
onIngested?: () => void;
}
// What a key may deliver; keys never read anything
const SCOPES: { id: ApiKeyScope; label: string; hint: string }[] = [
{ id: 'webhook:triage', label: '📥 Triage / Inbox', hint: 'Gedanken in die Inbox' },
{ id: 'webhook:task', label: '📋 Aufgaben', hint: 'Aufgaben direkt anlegen' },
{ id: 'webhook:list', label: '📝 Listeneinträge', hint: 'Einträge zu Listen hinzufügen' },
// What a key may do: deliver data (webhooks) or read and change tasks and lists for AI agents (MCP)
const SCOPES: { id: ApiKeyScope; label: string; hint: string; group: 'webhook' | 'agent' }[] = [
{ id: 'webhook:triage', label: '📥 Triage / Inbox', hint: 'Gedanken in die Inbox einliefern', group: 'webhook' },
{ id: 'webhook:task', label: '📋 Aufgaben anlegen', hint: 'Aufgaben direkt anlegen', group: 'webhook' },
{ id: 'webhook:list', label: '📝 Listeneinträge anlegen', hint: 'Einträge zu Listen hinzufügen', group: 'webhook' },
{ id: 'tasks:read', label: '📋 Aufgaben lesen', hint: 'Aufgabenlisten und Aufgaben lesen, suchen', group: 'agent' },
{ id: 'tasks:write', label: '✅ Aufgaben ändern', hint: 'Aufgaben anlegen, ändern und abhaken', group: 'agent' },
{ id: 'lists:read', label: '📝 Listen lesen', hint: 'Listen und ihre Einträge lesen', group: 'agent' },
{ id: 'lists:write', label: '🛒 Listen ändern', hint: 'Einträge hinzufügen, ändern und abhaken', group: 'agent' },
];
const scopeLabel = (id: string) => SCOPES.find((s) => s.id === id)?.label || id;
const isAgentScope = (sc: string) => SCOPES.find((s) => s.id === sc)?.group === 'agent';
type SimTarget = 'triage' | 'task' | 'list_item';
type McpClient = 'claude' | 'codex' | 'json';
// Personal API keys with scopes, a simulator and copyable examples (settings → Private Webhooks)
// Personal API keys with scopes (switches), MCP connection snippets, a webhook simulator and copyable
// examples (settings → Webhooks, KI-Agenten & Export)
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>('claude');
// Live Webhook Simulator State
const [simTitle, setSimTitle] = useState('Waschmaschine im Keller ist fertig!');
@@ -140,6 +150,26 @@ export const ApiKeysPanel: React.FC<ApiKeysPanelProps> = ({ onIngested }) => {
"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 */}
@@ -149,10 +179,10 @@ export const ApiKeysPanel: React.FC<ApiKeysPanelProps> = ({ onIngested }) => {
<Key className="w-4 h-4" />
</div>
<div>
<h3 className="text-sm font-bold text-main">Webhooks & Ingestion API-Schlüssel</h3>
<h3 className="text-sm font-bold text-main">API-Schlüssel für Webhooks und KI-Agenten</h3>
<p className="text-xs text-muted">
Verbinde Home Assistant, Zigbee2MQTT, n8n oder eigene Cronjobs mit MindSift. Ein Key kann nur Daten
einliefern, nie lesen; was genau, legst du pro Key fest.
Verbinde Home Assistant, n8n, eigene Skripte oder KI-Agenten (Claude Code, Codex …) mit MindSift. Was ein
Key darf, legst du pro Key fest; Notizen bleiben für Keys immer unerreichbar.
</p>
</div>
</div>
@@ -171,27 +201,32 @@ export const ApiKeysPanel: React.FC<ApiKeysPanelProps> = ({ onIngested }) => {
<span>API-Key erzeugen</span>
</button>
</div>
<div className="flex flex-wrap items-center gap-2" role="group" aria-label="Berechtigungen des Keys">
<span className="text-xs text-muted">Darf einliefern:</span>
{SCOPES.map((sc) => {
const on = newKeyScopes.includes(sc.id);
return (
<button
key={sc.id}
type="button"
role="checkbox"
aria-checked={on}
title={sc.hint}
onClick={() => toggleScope(sc.id)}
className={`h-11 sm:h-8 px-3 rounded-btn border text-xs inline-flex items-center gap-1.5 transition-colors ${
on ? 'border-accent bg-accent/10 text-main' : 'border-border text-muted hover:text-main'
}`}
>
{on && <Check className="w-3.5 h-3.5 text-accent" />}
{sc.label}
</button>
);
})}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3" role="group" aria-label="Berechtigungen des Keys">
{(
[
['webhook', 'Einliefern (Webhooks)', 'POST /api/v1/webhooks/ingest'],
['agent', 'KI-Agenten (MCP)', 'POST /api/v1/mcp'],
] as const
).map(([group, heading, path]) => (
<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 flex flex-wrap justify-between gap-x-2">
<span className="font-semibold">{heading}</span>
<code className="font-mono">{path}</code>
</p>
{SCOPES.filter((sc) => sc.group === group).map((sc) => {
const on = newKeyScopes.includes(sc.id);
return (
<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={on} onChange={() => toggleScope(sc.id)} title={sc.label} />
</div>
);
})}
</div>
))}
</div>
</form>
@@ -222,7 +257,12 @@ export const ApiKeysPanel: React.FC<ApiKeysPanelProps> = ({ onIngested }) => {
.split(',')
.filter(Boolean)
.map((sc) => (
<span key={sc} className="px-1.5 py-0.5 rounded-btn border border-border text-[10px] text-muted">
<span
key={sc}
className={`px-1.5 py-0.5 rounded-btn border text-[10px] ${
isAgentScope(sc) ? 'border-accent/40 text-accent' : 'border-border text-muted'
}`}
>
{scopeLabel(sc)}
</span>
))}
@@ -245,6 +285,56 @@ export const ApiKeysPanel: React.FC<ApiKeysPanelProps> = ({ onIngested }) => {
</div>
</div>
{/* AI agents (MCP) */}
<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">
<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 können offene
Aufgaben lesen, abhaken und anlegen sowie Listen führen. Dafür braucht der Key die Agenten-Berechtigungen.
</p>
</div>
</div>
<div className="flex flex-wrap gap-1.5 mb-2" role="radiogroup" aria-label="Agent">
{(
[
['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>
<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">list_tasks</code>,{' '}
<code className="font-mono">complete_task</code>, <code className="font-mono">create_task</code>,{' '}
<code className="font-mono">update_task</code>, <code className="font-mono">list_lists</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">search</code>,{' '}
<code className="font-mono">capture_to_inbox</code>. Der Agent sieht nur die Werkzeuge, für die der Key
berechtigt ist; Freigaben gelten wie für dich selbst. ChatGPT im Browser und claude.ai brauchen OAuth und
werden noch nicht unterstützt.
</p>
</div>
{/* Simulator */}
<div className="pt-4 border-t border-border">
<div className="flex items-center gap-3 mb-3">
@@ -1,6 +1,6 @@
import React from 'react';
import { Check } from 'lucide-react';
import { DATA_SECTIONS, DataSection } from '../../lib/types';
import { ToggleSwitch } from '../../components/ToggleSwitch';
interface SectionPickerProps {
value: DataSection[];
@@ -8,30 +8,27 @@ interface SectionPickerProps {
label?: string;
}
// Which parts of the own data a restore or import covers; at least one stays selected
// Which parts of the own data a restore or import covers; one switch per section, at least one stays on
export const SectionPicker: React.FC<SectionPickerProps> = ({ value, onChange, label = 'Bereiche' }) => (
<div className="flex flex-wrap items-center gap-1.5" role="group" aria-label={label}>
<span className="text-[11px] text-muted mr-1">{label}:</span>
{DATA_SECTIONS.map((sec) => {
const on = value.includes(sec.id);
return (
<button
key={sec.id}
type="button"
role="checkbox"
aria-checked={on}
onClick={() => {
const next = on ? value.filter((v) => v !== sec.id) : [...value, sec.id];
if (next.length > 0) onChange(next);
}}
className={`h-11 sm:h-7 px-2.5 rounded-btn border text-[11px] inline-flex items-center gap-1 transition-colors ${
on ? 'border-accent bg-accent/10 text-main' : 'border-border text-muted hover:text-main'
}`}
>
{on && <Check className="w-3 h-3 text-accent" />}
{sec.label}
</button>
);
})}
<div role="group" aria-label={label}>
<p className="text-[11px] text-muted mb-1.5">{label}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-1 border border-border rounded-card px-3 py-1.5 bg-panel">
{DATA_SECTIONS.map((sec) => {
const on = value.includes(sec.id);
return (
<div key={sec.id} className="flex items-center justify-between gap-3 min-h-[44px] sm:min-h-[36px]">
<span className="text-xs text-main">{sec.label}</span>
<ToggleSwitch
checked={on}
title={sec.label}
onChange={() => {
const next = on ? value.filter((v) => v !== sec.id) : [...value, sec.id];
if (next.length > 0) onChange(next);
}}
/>
</div>
);
})}
</div>
</div>
);
@@ -582,7 +582,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<div>
<h1 className="text-base font-bold text-main tracking-tight">Kontoeinstellungen</h1>
<p className="text-xs text-muted">
Persönliches Profil, Design-Themes, Aufgaben-Alarme, Private Webhooks & Daten-Backup
Persönliches Profil, Design-Themes, Aufgaben-Alarme, Webhooks, KI-Agenten & Daten-Backup
</p>
</div>
</div>
@@ -651,8 +651,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
>
<Key className="w-4 h-4 text-accent flex-shrink-0" />
<div>
<p className="font-semibold">Private Webhooks & Export</p>
<p className="text-[10px] text-faint">API-Keys, Curl & JSON-Backup</p>
<p className="font-semibold">Webhooks, KI-Agenten & Export</p>
<p className="text-[10px] text-faint">API-Keys, MCP, Curl & JSON-Backup</p>
</div>
</button>
</aside>
@@ -1855,11 +1855,11 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<div className="space-y-5">
<div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-main">
Private Webhooks (Eingehend) & Daten-Export
Webhooks, KI-Agenten & Daten-Export
</h2>
<p className="text-muted mt-0.5">
Erstelle persönliche API-Schlüssel, um aus externen Skripten, Server-Monitoring
oder Discord-Bots automatisch Aufgaben und Gedanken in dein MindSift einzuspeisen.
Erstelle persönliche API-Schlüssel, um aus Skripten, Smarthome oder Bots Aufgaben und Gedanken
einzuspeisen oder KI-Agenten wie Claude Code und Codex an deine Aufgaben und Listen zu lassen.
</p>
</div>
@@ -24,18 +24,27 @@ describe('ApiKeysPanel', () => {
it('shows the scopes of existing keys as badges', async () => {
render(<ApiKeysPanel />);
expect(await screen.findByText('Rauchmelder')).toBeTruthy();
expect(screen.getAllByText('📋 Aufgaben').length).toBeGreaterThan(1); // chip + badge
expect(screen.getAllByText('📋 Aufgaben anlegen').length).toBeGreaterThan(1); // switch label + badge
});
it('creates a key with the chosen scopes and never without one', async () => {
render(<ApiKeysPanel />);
const triage = screen.getByRole('checkbox', { name: /Triage/ });
const triage = screen.getByRole('switch', { name: /Triage/ });
fireEvent.click(triage); // last remaining scope cannot be switched off
expect(triage.getAttribute('aria-checked')).toBe('true');
fireEvent.click(screen.getByRole('checkbox', { name: /Listeneinträge/ }));
fireEvent.click(screen.getByRole('switch', { name: /Listeneinträge anlegen/ }));
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /API-Key erzeugen/ }));
});
expect(createAPIKey).toHaveBeenCalledWith('Home Assistant', ['webhook:triage', 'webhook:list']);
});
it('offers agent scopes and shows how to connect an MCP client', async () => {
render(<ApiKeysPanel />);
fireEvent.click(screen.getByRole('switch', { name: /Aufgaben ändern/ }));
expect(screen.getByRole('switch', { name: /Aufgaben ändern/ }).getAttribute('aria-checked')).toBe('true');
expect(screen.getByText(/claude mcp add --transport http mindsift/)).toBeTruthy();
fireEvent.click(screen.getByRole('radio', { name: 'OpenAI Codex' }));
expect(screen.getByText(/\[mcp_servers\.mindsift\]/)).toBeTruthy();
});
});
@@ -0,0 +1,16 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { SectionPicker } from '../SectionPicker';
describe('SectionPicker', () => {
it('uses switches and keeps at least one section on', () => {
const onChange = vi.fn();
render(<SectionPicker value={['notes']} onChange={onChange} />);
const notes = screen.getByRole('switch', { name: 'Notizen' });
expect(notes.getAttribute('aria-checked')).toBe('true');
fireEvent.click(notes); // last one cannot be switched off
expect(onChange).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('switch', { name: 'Listen' }));
expect(onChange).toHaveBeenCalledWith(['notes', 'lists']);
});
});