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

+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">