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 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:
1 parent
5982563579
commit
8faed7a160
22 files changed
+1243
-147
No files matched your search
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user