feat(mcp): allow minting and consenting to health:write connector tokens

Let a user opt a connector token into logging. The settings card gains a
write toggle; the mint endpoint accepts a closed scope choice (read, or
read_write) and builds the permission array explicitly — it can mint only
[health:read] or [health:read, health:write], never a wildcard or any other
grant. The OAuth flow grants health:write only when a client requests it, and
the consent screen states in plain language that the app will be able to read
AND log/write health data when write is requested; the token endpoint grants
exactly the approved scopes and never widens. Read-only behaviour is
unchanged. Add the English and German strings for the new toggle and warning.
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 17:38:51 +02:00
1 parent 6de61de010
commit ddfe9170bd
11 files changed
+195 -19

No files matched your search

+23 -1
View File
@@ -257,6 +257,7 @@ function McpTokensCard() {
const { isAuthenticated } = useAuth();
const queryClient = useQueryClient();
const [newName, setNewName] = useState("");
const [allowWrite, setAllowWrite] = useState(false);
const [newToken, setNewToken] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const [tokenMsg, setTokenMsg] = useState<string | null>(null);
@@ -277,12 +278,17 @@ function McpTokensCard() {
const res = await apiFetchRaw("/api/mcp/tokens", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newName.trim() }),
body: JSON.stringify({
name: newName.trim(),
// Closed choice — read-only, or read + the audience-bound write scope.
scope: allowWrite ? "read_write" : "read",
}),
});
const json = await res.json();
if (res.ok) {
setNewToken(json.data.token);
setNewName("");
setAllowWrite(false);
queryClient.invalidateQueries({ queryKey: queryKeys.mcpTokens() });
} else {
setTokenMsg(json.error || t("common.error"));
@@ -336,6 +342,22 @@ function McpTokensCard() {
{t("settings.mcp.scopeNote")}
</p>
<div className="border-border bg-muted/30 space-y-2 rounded-lg border p-3">
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-medium">
{t("settings.mcp.writeScopeToggleLabel")}
</span>
<Switch
checked={allowWrite}
onCheckedChange={setAllowWrite}
aria-label={t("settings.mcp.writeScopeToggleLabel")}
/>
</div>
<p className="text-muted-foreground text-[11px] leading-relaxed">
{t("settings.mcp.writeScopeNote")}
</p>
</div>
<form onSubmit={handleCreate} className="flex items-center gap-2">
<Input
value={newName}