feat(mcp): OAuth so claude.ai and ChatGPT can connect as connectors
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m49s

MindSift becomes the OAuth authorization server for its own MCP
endpoint: discovery (RFC 9728/8414), dynamic client registration,
authorization code with PKCE (S256), rotating refresh tokens with reuse
detection. The consent page lives in the app at /oauth/authorize. A
grant is an api_keys entry (kind 'oauth'), so scopes, origin badges,
renaming and revoking work as for keys; tokens are valid for /mcp only.
Unused connections expire after OAUTH_IDLE_DAYS (default 90). Requires
BASE_URL; off in demo mode. Release 0.51.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 20:01:32 +01:00
1 parent 994f6dc554
commit 75aea4a7d4
31 files changed
+1581 -49

No files matched your search

+27
View File
@@ -1,5 +1,6 @@
import {
APIKey,
OAuthAuthorizeInfo,
AuthStatus,
DashboardStats,
Document,
@@ -1322,6 +1323,32 @@ export const api = {
await request(`/api/v1/keys/${id}`, { method: 'DELETE' });
},
// --- OAUTH CONSENT (claude.ai, ChatGPT connect to MCP) ---
/** query: the search string of /oauth/authorize as the client sent it */
async getOAuthAuthorizeInfo(query: string): Promise<OAuthAuthorizeInfo> {
return request<OAuthAuthorizeInfo>(`/api/v1/oauth/authorize${query}`);
},
/** Allow (with the chosen rights) or refuse; returns where to send the browser */
async answerOAuthAuthorize(query: string, approve: boolean, scopes: ApiKeyScope[]): Promise<{ redirect: string }> {
const q = new URLSearchParams(query);
return request<{ redirect: string }>('/api/v1/oauth/authorize', {
method: 'POST',
body: JSON.stringify({
responseType: q.get('response_type') || '',
clientId: q.get('client_id') || '',
redirectUri: q.get('redirect_uri') || '',
codeChallenge: q.get('code_challenge') || '',
codeChallengeMethod: q.get('code_challenge_method') || '',
state: q.get('state') || '',
scope: q.get('scope') || '',
resource: q.get('resource') || '',
approve,
scopes,
}),
});
},
/** Simulator: sends like an external system would, with the given key or else the own session. */
async testWebhookIngest(
apiKey: string | null,