feat(mcp): forbind med kontoen i stedet for med et token

Claude's forbindelsesdialog prøver OAuth først. Indtil nu var svaret 404,
og eneste vej ind var at lave et token og sætte det ind i en adresse. Nu
er instansen sin egen autorisationsserver for MCP-klienter:

- /.well-known/oauth-protected-resource(/mcp) og
  /.well-known/oauth-authorization-server peger på login'et; en 401 fra
  /mcp bærer WWW-Authenticate med resource_metadata
- /oauth/register (RFC 7591), kun offentlige klienter; registreringer,
  ingen godkender, ryddes efter et døgn
- /oauth/authorize sender til /forbind, som logger ind med det almindelige
  login og spørger "Claude vil forbinde til din konto" — og viser, hvor
  adgangen sendes hen, fordi navnet er klientens eget
- /oauth/token: authorization code + S256 PKCE, koden gælder ét minut og
  én gang; ud kommer et almindeligt API-token, listet under Tokens med
  klientens navn, og en ny forbindelse fra samme klient erstatter det
- /mcp tager nu også Authorization: Bearer; ?key= virker stadig

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
kristianwind
2026-10-31 22:49:48 +01:00
co-authored by Claude Opus 5.5
parent 4a2aa81725
commit e7df876c4a
19 changed files with 1792 additions and 55 deletions
+6
View File
@@ -445,6 +445,12 @@ export const api = {
post('/tokens', { name, expires_in_days: expiresInDays ?? 0 }),
deleteToken: (id) => del(`/tokens/${id}`),
// --- signing an MCP client in with the account (routes/forbind)
// The query goes over unchanged: the server reads it with the same function
// that checked it on the way in, so the two cannot disagree about it.
oauthRequest: (query) => get(`/oauth/request${query}`),
oauthApprove: (query, approve) => post('/oauth/approve', { query, approve }),
// --- live sync, for the app (see stores.svelte.js)
wsTicket: () => post('/ws-ticket'),
+14
View File
@@ -498,6 +498,20 @@ export const da = {
'ai.keyNewHint': 'Gemmes på serveren og vises aldrig igen.',
'tokens.lastUsed': 'sidst brugt {when}',
'tokens.neverUsed': 'aldrig brugt',
'connect.title': 'Forbind',
'connect.heading': '{name} vil forbinde til din konto',
'connect.what': 'Den kan læse og ændre dine opgaver og noter — med præcis dine rettigheder, ikke flere.',
'connect.whatNotes': 'Den kan læse og ændre dine noter — med præcis dine rettigheder, ikke flere.',
'tokens.signInHint': 'Til Claude og andre MCP-programmer behøver du ikke et token: tilføj adressen nedenfor som forbindelse, og log ind, når programmet beder om det.',
'connect.account': 'Konto',
'connect.sentTo': 'Sendes til',
'connect.revoke': 'Du kan afbryde forbindelsen når som helst under Indstillinger → Tokens.',
'connect.allow': 'Tillad',
'connect.deny': 'Afvis',
'connect.brokenTitle': 'Forbindelsen kan ikke laves',
'connect.broken': 'Programmet, der sendte dig hertil, bad om noget, der ikke kan godkendes. Fjern forbindelsen i programmet, og tilføj den igen.',
'connect.toApp': 'Til appen',
'tokens.connected': 'forbundet med din konto',
'tokens.expired': 'udløbet {when}',
'tokens.expiresOn': 'udløber {when}',
'tokens.revokeNamed': 'Tilbagekald {name}',
+14
View File
@@ -492,6 +492,20 @@ export const en = {
'ai.keyNewHint': 'Stored on the server and never shown again.',
'tokens.lastUsed': 'last used {when}',
'tokens.neverUsed': 'never used',
'connect.title': 'Connect',
'connect.heading': '{name} wants to connect to your account',
'connect.what': 'It can read and change your tasks and notes — with exactly your permissions, no more.',
'connect.whatNotes': 'It can read and change your notes — with exactly your permissions, no more.',
'tokens.signInHint': 'Claude and other MCP programs do not need a token: add the address below as a connector, and sign in when it asks.',
'connect.account': 'Account',
'connect.sentTo': 'Sent to',
'connect.revoke': 'You can disconnect it at any time under Settings → Tokens.',
'connect.allow': 'Allow',
'connect.deny': 'Deny',
'connect.brokenTitle': 'This connection cannot be made',
'connect.broken': 'The program that sent you here asked for something that cannot be approved. Remove the connection in that program and add it again.',
'connect.toApp': 'To the app',
'tokens.connected': 'connected with your account',
'tokens.expired': 'expired {when}',
'tokens.expiresOn': 'expires {when}',
'tokens.revokeNamed': 'Revoke {name}',