basic-memory 0.14.2__py3-none-any.whl → 0.14.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (51) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/env.py +3 -1
  3. basic_memory/api/app.py +4 -1
  4. basic_memory/api/routers/management_router.py +3 -1
  5. basic_memory/api/routers/project_router.py +21 -13
  6. basic_memory/cli/app.py +3 -3
  7. basic_memory/cli/commands/__init__.py +1 -2
  8. basic_memory/cli/commands/db.py +5 -5
  9. basic_memory/cli/commands/import_chatgpt.py +3 -2
  10. basic_memory/cli/commands/import_claude_conversations.py +3 -1
  11. basic_memory/cli/commands/import_claude_projects.py +3 -1
  12. basic_memory/cli/commands/import_memory_json.py +5 -2
  13. basic_memory/cli/commands/mcp.py +3 -15
  14. basic_memory/cli/commands/project.py +41 -0
  15. basic_memory/cli/commands/status.py +4 -1
  16. basic_memory/cli/commands/sync.py +10 -2
  17. basic_memory/cli/main.py +0 -1
  18. basic_memory/config.py +46 -31
  19. basic_memory/db.py +2 -6
  20. basic_memory/deps.py +3 -2
  21. basic_memory/importers/chatgpt_importer.py +19 -9
  22. basic_memory/importers/memory_json_importer.py +22 -7
  23. basic_memory/mcp/async_client.py +22 -2
  24. basic_memory/mcp/project_session.py +6 -4
  25. basic_memory/mcp/prompts/__init__.py +0 -2
  26. basic_memory/mcp/server.py +8 -71
  27. basic_memory/mcp/tools/move_note.py +24 -12
  28. basic_memory/mcp/tools/read_content.py +16 -0
  29. basic_memory/mcp/tools/read_note.py +12 -0
  30. basic_memory/mcp/tools/sync_status.py +3 -2
  31. basic_memory/mcp/tools/write_note.py +9 -1
  32. basic_memory/models/project.py +3 -3
  33. basic_memory/repository/project_repository.py +18 -0
  34. basic_memory/schemas/importer.py +1 -0
  35. basic_memory/services/entity_service.py +49 -3
  36. basic_memory/services/initialization.py +0 -75
  37. basic_memory/services/project_service.py +85 -28
  38. basic_memory/sync/background_sync.py +4 -3
  39. basic_memory/sync/sync_service.py +50 -1
  40. basic_memory/utils.py +105 -4
  41. {basic_memory-0.14.2.dist-info → basic_memory-0.14.3.dist-info}/METADATA +2 -2
  42. {basic_memory-0.14.2.dist-info → basic_memory-0.14.3.dist-info}/RECORD +45 -51
  43. basic_memory/cli/commands/auth.py +0 -136
  44. basic_memory/mcp/auth_provider.py +0 -270
  45. basic_memory/mcp/external_auth_provider.py +0 -321
  46. basic_memory/mcp/prompts/sync_status.py +0 -112
  47. basic_memory/mcp/supabase_auth_provider.py +0 -463
  48. basic_memory/services/migration_service.py +0 -168
  49. {basic_memory-0.14.2.dist-info → basic_memory-0.14.3.dist-info}/WHEEL +0 -0
  50. {basic_memory-0.14.2.dist-info → basic_memory-0.14.3.dist-info}/entry_points.txt +0 -0
  51. {basic_memory-0.14.2.dist-info → basic_memory-0.14.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,270 +0,0 @@
1
- """OAuth authentication provider for Basic Memory MCP server."""
2
-
3
- import secrets
4
- from datetime import datetime, timedelta, timezone
5
- from typing import Dict, Optional
6
-
7
- import jwt
8
- from mcp.server.auth.provider import (
9
- OAuthAuthorizationServerProvider,
10
- AuthorizationParams,
11
- AuthorizationCode,
12
- RefreshToken,
13
- AccessToken,
14
- )
15
- from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
16
- from loguru import logger
17
-
18
-
19
- class BasicMemoryAuthorizationCode(AuthorizationCode):
20
- """Extended authorization code with additional metadata."""
21
-
22
- issuer_state: Optional[str] = None
23
-
24
-
25
- class BasicMemoryRefreshToken(RefreshToken):
26
- """Extended refresh token with additional metadata."""
27
-
28
- pass
29
-
30
-
31
- class BasicMemoryAccessToken(AccessToken):
32
- """Extended access token with additional metadata."""
33
-
34
- pass
35
-
36
-
37
- class BasicMemoryOAuthProvider(
38
- OAuthAuthorizationServerProvider[
39
- BasicMemoryAuthorizationCode, BasicMemoryRefreshToken, BasicMemoryAccessToken
40
- ]
41
- ):
42
- """OAuth provider for Basic Memory MCP server.
43
-
44
- This is a simple in-memory implementation that can be extended
45
- to integrate with external OAuth providers or use persistent storage.
46
- """
47
-
48
- def __init__(self, issuer_url: str = "http://localhost:8000", secret_key: Optional[str] = None):
49
- self.issuer_url = issuer_url
50
- # Use environment variable for secret key if available, otherwise generate
51
- import os
52
-
53
- self.secret_key = (
54
- secret_key or os.getenv("FASTMCP_AUTH_SECRET_KEY") or secrets.token_urlsafe(32)
55
- )
56
-
57
- # In-memory storage - in production, use a proper database
58
- self.clients: Dict[str, OAuthClientInformationFull] = {}
59
- self.authorization_codes: Dict[str, BasicMemoryAuthorizationCode] = {}
60
- self.refresh_tokens: Dict[str, BasicMemoryRefreshToken] = {}
61
- self.access_tokens: Dict[str, BasicMemoryAccessToken] = {}
62
-
63
- async def get_client(self, client_id: str) -> Optional[OAuthClientInformationFull]:
64
- """Get a client by ID."""
65
- return self.clients.get(client_id)
66
-
67
- async def register_client(self, client_info: OAuthClientInformationFull) -> None:
68
- """Register a new OAuth client."""
69
- # Generate client ID if not provided
70
- if not client_info.client_id:
71
- client_info.client_id = secrets.token_urlsafe(16)
72
-
73
- # Generate client secret if not provided
74
- if not client_info.client_secret:
75
- client_info.client_secret = secrets.token_urlsafe(32)
76
-
77
- self.clients[client_info.client_id] = client_info
78
- logger.info(f"Registered OAuth client: {client_info.client_id}")
79
-
80
- async def authorize(
81
- self, client: OAuthClientInformationFull, params: AuthorizationParams
82
- ) -> str:
83
- """Create an authorization URL for the OAuth flow.
84
-
85
- For basic-memory, we'll implement a simple authorization flow.
86
- In production, this might redirect to an external provider.
87
- """
88
- # Generate authorization code
89
- auth_code = secrets.token_urlsafe(32)
90
-
91
- # Store authorization code with metadata
92
- self.authorization_codes[auth_code] = BasicMemoryAuthorizationCode(
93
- code=auth_code,
94
- scopes=params.scopes or [],
95
- expires_at=(datetime.now(timezone.utc) + timedelta(minutes=10)).timestamp(),
96
- client_id=client.client_id,
97
- code_challenge=params.code_challenge,
98
- redirect_uri=params.redirect_uri,
99
- redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
100
- issuer_state=params.state,
101
- )
102
-
103
- # In a real implementation, we'd redirect to an authorization page
104
- # For now, we'll just return the redirect URL with the code
105
- redirect_uri = str(params.redirect_uri)
106
- separator = "&" if "?" in redirect_uri else "?"
107
-
108
- auth_url = f"{redirect_uri}{separator}code={auth_code}"
109
- if params.state:
110
- auth_url += f"&state={params.state}"
111
-
112
- return auth_url
113
-
114
- async def load_authorization_code(
115
- self, client: OAuthClientInformationFull, authorization_code: str
116
- ) -> Optional[BasicMemoryAuthorizationCode]:
117
- """Load an authorization code."""
118
- code = self.authorization_codes.get(authorization_code)
119
-
120
- if code and code.client_id == client.client_id:
121
- # Check if expired
122
- if datetime.now(timezone.utc).timestamp() > code.expires_at:
123
- del self.authorization_codes[authorization_code]
124
- return None
125
- return code
126
-
127
- return None
128
-
129
- async def exchange_authorization_code(
130
- self, client: OAuthClientInformationFull, authorization_code: BasicMemoryAuthorizationCode
131
- ) -> OAuthToken:
132
- """Exchange an authorization code for tokens."""
133
- # Generate tokens
134
- access_token = self._generate_access_token(client.client_id, authorization_code.scopes)
135
- refresh_token = secrets.token_urlsafe(32)
136
-
137
- # Store tokens
138
- expires_at = (datetime.now(timezone.utc) + timedelta(hours=1)).timestamp()
139
-
140
- self.access_tokens[access_token] = BasicMemoryAccessToken(
141
- token=access_token,
142
- client_id=client.client_id,
143
- scopes=authorization_code.scopes,
144
- expires_at=int(expires_at),
145
- )
146
-
147
- self.refresh_tokens[refresh_token] = BasicMemoryRefreshToken(
148
- token=refresh_token,
149
- client_id=client.client_id,
150
- scopes=authorization_code.scopes,
151
- )
152
-
153
- # Remove used authorization code
154
- del self.authorization_codes[authorization_code.code]
155
-
156
- return OAuthToken(
157
- access_token=access_token,
158
- token_type="bearer",
159
- expires_in=3600, # 1 hour
160
- refresh_token=refresh_token,
161
- scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
162
- )
163
-
164
- async def load_refresh_token(
165
- self, client: OAuthClientInformationFull, refresh_token: str
166
- ) -> Optional[BasicMemoryRefreshToken]:
167
- """Load a refresh token."""
168
- token = self.refresh_tokens.get(refresh_token)
169
-
170
- if token and token.client_id == client.client_id:
171
- return token
172
-
173
- return None
174
-
175
- async def exchange_refresh_token(
176
- self,
177
- client: OAuthClientInformationFull,
178
- refresh_token: BasicMemoryRefreshToken,
179
- scopes: list[str],
180
- ) -> OAuthToken:
181
- """Exchange a refresh token for new tokens."""
182
- # Use requested scopes or original scopes
183
- token_scopes = scopes if scopes else refresh_token.scopes
184
-
185
- # Generate new tokens
186
- new_access_token = self._generate_access_token(client.client_id, token_scopes)
187
- new_refresh_token = secrets.token_urlsafe(32)
188
-
189
- # Store new tokens
190
- expires_at = (datetime.now(timezone.utc) + timedelta(hours=1)).timestamp()
191
-
192
- self.access_tokens[new_access_token] = BasicMemoryAccessToken(
193
- token=new_access_token,
194
- client_id=client.client_id,
195
- scopes=token_scopes,
196
- expires_at=int(expires_at),
197
- )
198
-
199
- self.refresh_tokens[new_refresh_token] = BasicMemoryRefreshToken(
200
- token=new_refresh_token,
201
- client_id=client.client_id,
202
- scopes=token_scopes,
203
- )
204
-
205
- # Remove old tokens
206
- del self.refresh_tokens[refresh_token.token]
207
-
208
- return OAuthToken(
209
- access_token=new_access_token,
210
- token_type="bearer",
211
- expires_in=3600, # 1 hour
212
- refresh_token=new_refresh_token,
213
- scope=" ".join(token_scopes) if token_scopes else None,
214
- )
215
-
216
- async def load_access_token(self, token: str) -> Optional[BasicMemoryAccessToken]:
217
- """Load and validate an access token."""
218
- logger.debug("Loading access token, checking in-memory store first")
219
- access_token = self.access_tokens.get(token)
220
-
221
- if access_token:
222
- # Check if expired
223
- if access_token.expires_at and datetime.now(timezone.utc).timestamp() > access_token.expires_at:
224
- logger.debug("Token found in memory but expired, removing")
225
- del self.access_tokens[token]
226
- return None
227
- logger.debug("Token found in memory and valid")
228
- return access_token
229
-
230
- # Try to decode as JWT
231
- logger.debug("Token not in memory, attempting JWT decode with secret key")
232
- try:
233
- # Decode with audience verification - PyJWT expects the audience to match
234
- payload = jwt.decode(
235
- token,
236
- self.secret_key,
237
- algorithms=["HS256"],
238
- audience="basic-memory", # Expecting this audience
239
- issuer=self.issuer_url, # And this issuer
240
- )
241
- logger.debug(f"JWT decoded successfully: {payload}")
242
- return BasicMemoryAccessToken(
243
- token=token,
244
- client_id=payload.get("sub", ""),
245
- scopes=payload.get("scopes", []),
246
- expires_at=payload.get("exp"),
247
- )
248
- except jwt.InvalidTokenError as e:
249
- logger.error(f"JWT decode failed: {e}")
250
- return None
251
-
252
- async def revoke_token(self, token: BasicMemoryAccessToken | BasicMemoryRefreshToken) -> None:
253
- """Revoke an access or refresh token."""
254
- if isinstance(token, BasicMemoryAccessToken):
255
- self.access_tokens.pop(token.token, None)
256
- else:
257
- self.refresh_tokens.pop(token.token, None)
258
-
259
- def _generate_access_token(self, client_id: str, scopes: list[str]) -> str:
260
- """Generate a JWT access token."""
261
- payload = {
262
- "iss": self.issuer_url,
263
- "sub": client_id,
264
- "aud": "basic-memory",
265
- "exp": datetime.now(timezone.utc) + timedelta(hours=1),
266
- "iat": datetime.now(timezone.utc),
267
- "scopes": scopes,
268
- }
269
-
270
- return jwt.encode(payload, self.secret_key, algorithm="HS256")
@@ -1,321 +0,0 @@
1
- """External OAuth provider integration for Basic Memory MCP server."""
2
-
3
- import os
4
- from typing import Optional, Dict, Any
5
- from dataclasses import dataclass
6
-
7
- import httpx
8
- from loguru import logger
9
- from mcp.server.auth.provider import (
10
- OAuthAuthorizationServerProvider,
11
- AuthorizationParams,
12
- AuthorizationCode,
13
- RefreshToken,
14
- AccessToken,
15
- construct_redirect_uri,
16
- )
17
- from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
18
-
19
-
20
- @dataclass
21
- class ExternalAuthorizationCode(AuthorizationCode):
22
- """Authorization code with external provider metadata."""
23
-
24
- external_code: Optional[str] = None
25
- state: Optional[str] = None
26
-
27
-
28
- @dataclass
29
- class ExternalRefreshToken(RefreshToken):
30
- """Refresh token with external provider metadata."""
31
-
32
- external_token: Optional[str] = None
33
-
34
-
35
- @dataclass
36
- class ExternalAccessToken(AccessToken):
37
- """Access token with external provider metadata."""
38
-
39
- external_token: Optional[str] = None
40
-
41
-
42
- class ExternalOAuthProvider(
43
- OAuthAuthorizationServerProvider[
44
- ExternalAuthorizationCode, ExternalRefreshToken, ExternalAccessToken
45
- ]
46
- ):
47
- """OAuth provider that delegates to external OAuth providers.
48
-
49
- This provider can integrate with services like:
50
- - GitHub OAuth
51
- - Google OAuth
52
- - Auth0
53
- - Okta
54
- """
55
-
56
- def __init__(
57
- self,
58
- issuer_url: str,
59
- external_provider: str,
60
- external_client_id: str,
61
- external_client_secret: str,
62
- external_authorize_url: str,
63
- external_token_url: str,
64
- external_userinfo_url: Optional[str] = None,
65
- ):
66
- self.issuer_url = issuer_url
67
- self.external_provider = external_provider
68
- self.external_client_id = external_client_id
69
- self.external_client_secret = external_client_secret
70
- self.external_authorize_url = external_authorize_url
71
- self.external_token_url = external_token_url
72
- self.external_userinfo_url = external_userinfo_url
73
-
74
- # In-memory storage - in production, use a database
75
- self.clients: Dict[str, OAuthClientInformationFull] = {}
76
- self.codes: Dict[str, ExternalAuthorizationCode] = {}
77
- self.tokens: Dict[str, Any] = {}
78
-
79
- self.http_client = httpx.AsyncClient()
80
-
81
- async def get_client(self, client_id: str) -> Optional[OAuthClientInformationFull]:
82
- """Get a client by ID."""
83
- return self.clients.get(client_id)
84
-
85
- async def register_client(self, client_info: OAuthClientInformationFull) -> None:
86
- """Register a new OAuth client."""
87
- self.clients[client_info.client_id] = client_info
88
- logger.info(f"Registered external OAuth client: {client_info.client_id}")
89
-
90
- async def authorize(
91
- self, client: OAuthClientInformationFull, params: AuthorizationParams
92
- ) -> str:
93
- """Create authorization URL redirecting to external provider."""
94
- # Store authorization request
95
- import secrets
96
-
97
- state = secrets.token_urlsafe(32)
98
-
99
- self.codes[state] = ExternalAuthorizationCode(
100
- code=state,
101
- scopes=params.scopes or [],
102
- expires_at=0, # Will be set by external provider
103
- client_id=client.client_id,
104
- code_challenge=params.code_challenge,
105
- redirect_uri=params.redirect_uri,
106
- redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
107
- state=params.state,
108
- )
109
-
110
- # Build external provider URL
111
- external_params = {
112
- "client_id": self.external_client_id,
113
- "redirect_uri": f"{self.issuer_url}/callback",
114
- "response_type": "code",
115
- "state": state,
116
- "scope": " ".join(params.scopes or []),
117
- }
118
-
119
- return construct_redirect_uri(self.external_authorize_url, **external_params)
120
-
121
- async def handle_callback(self, code: str, state: str) -> str:
122
- """Handle callback from external provider."""
123
- # Get original authorization request
124
- auth_code = self.codes.get(state)
125
- if not auth_code:
126
- raise ValueError("Invalid state parameter")
127
-
128
- # Exchange code with external provider
129
- token_data = {
130
- "grant_type": "authorization_code",
131
- "code": code,
132
- "redirect_uri": f"{self.issuer_url}/callback",
133
- "client_id": self.external_client_id,
134
- "client_secret": self.external_client_secret,
135
- }
136
-
137
- response = await self.http_client.post(
138
- self.external_token_url,
139
- data=token_data,
140
- )
141
- response.raise_for_status()
142
- external_tokens = response.json()
143
-
144
- # Store external tokens
145
- import secrets
146
-
147
- internal_code = secrets.token_urlsafe(32)
148
-
149
- self.codes[internal_code] = ExternalAuthorizationCode(
150
- code=internal_code,
151
- scopes=auth_code.scopes,
152
- expires_at=0,
153
- client_id=auth_code.client_id,
154
- code_challenge=auth_code.code_challenge,
155
- redirect_uri=auth_code.redirect_uri,
156
- redirect_uri_provided_explicitly=auth_code.redirect_uri_provided_explicitly,
157
- external_code=code,
158
- state=auth_code.state,
159
- )
160
-
161
- self.tokens[internal_code] = external_tokens
162
-
163
- # Redirect to original client
164
- return construct_redirect_uri(
165
- str(auth_code.redirect_uri),
166
- code=internal_code,
167
- state=auth_code.state,
168
- )
169
-
170
- async def load_authorization_code(
171
- self, client: OAuthClientInformationFull, authorization_code: str
172
- ) -> Optional[ExternalAuthorizationCode]:
173
- """Load an authorization code."""
174
- code = self.codes.get(authorization_code)
175
- if code and code.client_id == client.client_id:
176
- return code
177
- return None
178
-
179
- async def exchange_authorization_code(
180
- self, client: OAuthClientInformationFull, authorization_code: ExternalAuthorizationCode
181
- ) -> OAuthToken:
182
- """Exchange authorization code for tokens."""
183
- # Get stored external tokens
184
- external_tokens = self.tokens.get(authorization_code.code)
185
- if not external_tokens:
186
- raise ValueError("No tokens found for authorization code")
187
-
188
- # Map external tokens to MCP tokens
189
- access_token = external_tokens.get("access_token")
190
- refresh_token = external_tokens.get("refresh_token")
191
- expires_in = external_tokens.get("expires_in", 3600)
192
-
193
- # Store the mapping
194
- self.tokens[access_token] = {
195
- "client_id": client.client_id,
196
- "external_token": access_token,
197
- "scopes": authorization_code.scopes,
198
- }
199
-
200
- if refresh_token:
201
- self.tokens[refresh_token] = {
202
- "client_id": client.client_id,
203
- "external_token": refresh_token,
204
- "scopes": authorization_code.scopes,
205
- }
206
-
207
- # Clean up authorization code
208
- del self.codes[authorization_code.code]
209
-
210
- return OAuthToken(
211
- access_token=access_token,
212
- token_type="bearer",
213
- expires_in=expires_in,
214
- refresh_token=refresh_token,
215
- scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
216
- )
217
-
218
- async def load_refresh_token(
219
- self, client: OAuthClientInformationFull, refresh_token: str
220
- ) -> Optional[ExternalRefreshToken]:
221
- """Load a refresh token."""
222
- token_info = self.tokens.get(refresh_token)
223
- if token_info and token_info["client_id"] == client.client_id:
224
- return ExternalRefreshToken(
225
- token=refresh_token,
226
- client_id=client.client_id,
227
- scopes=token_info["scopes"],
228
- external_token=token_info.get("external_token"),
229
- )
230
- return None
231
-
232
- async def exchange_refresh_token(
233
- self,
234
- client: OAuthClientInformationFull,
235
- refresh_token: ExternalRefreshToken,
236
- scopes: list[str],
237
- ) -> OAuthToken:
238
- """Exchange refresh token for new tokens."""
239
- # Exchange with external provider
240
- token_data = {
241
- "grant_type": "refresh_token",
242
- "refresh_token": refresh_token.external_token or refresh_token.token,
243
- "client_id": self.external_client_id,
244
- "client_secret": self.external_client_secret,
245
- }
246
-
247
- response = await self.http_client.post(
248
- self.external_token_url,
249
- data=token_data,
250
- )
251
- response.raise_for_status()
252
- external_tokens = response.json()
253
-
254
- # Update stored tokens
255
- new_access_token = external_tokens.get("access_token")
256
- new_refresh_token = external_tokens.get("refresh_token", refresh_token.token)
257
- expires_in = external_tokens.get("expires_in", 3600)
258
-
259
- self.tokens[new_access_token] = {
260
- "client_id": client.client_id,
261
- "external_token": new_access_token,
262
- "scopes": scopes or refresh_token.scopes,
263
- }
264
-
265
- if new_refresh_token != refresh_token.token:
266
- self.tokens[new_refresh_token] = {
267
- "client_id": client.client_id,
268
- "external_token": new_refresh_token,
269
- "scopes": scopes or refresh_token.scopes,
270
- }
271
- del self.tokens[refresh_token.token]
272
-
273
- return OAuthToken(
274
- access_token=new_access_token,
275
- token_type="bearer",
276
- expires_in=expires_in,
277
- refresh_token=new_refresh_token,
278
- scope=" ".join(scopes or refresh_token.scopes),
279
- )
280
-
281
- async def load_access_token(self, token: str) -> Optional[ExternalAccessToken]:
282
- """Load and validate an access token."""
283
- token_info = self.tokens.get(token)
284
- if token_info:
285
- return ExternalAccessToken(
286
- token=token,
287
- client_id=token_info["client_id"],
288
- scopes=token_info["scopes"],
289
- external_token=token_info.get("external_token"),
290
- )
291
- return None
292
-
293
- async def revoke_token(self, token: ExternalAccessToken | ExternalRefreshToken) -> None:
294
- """Revoke a token."""
295
- self.tokens.pop(token.token, None)
296
-
297
-
298
- def create_github_provider() -> ExternalOAuthProvider:
299
- """Create an OAuth provider for GitHub integration."""
300
- return ExternalOAuthProvider(
301
- issuer_url=os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000"),
302
- external_provider="github",
303
- external_client_id=os.getenv("GITHUB_CLIENT_ID", ""),
304
- external_client_secret=os.getenv("GITHUB_CLIENT_SECRET", ""),
305
- external_authorize_url="https://github.com/login/oauth/authorize",
306
- external_token_url="https://github.com/login/oauth/access_token",
307
- external_userinfo_url="https://api.github.com/user",
308
- )
309
-
310
-
311
- def create_google_provider() -> ExternalOAuthProvider:
312
- """Create an OAuth provider for Google integration."""
313
- return ExternalOAuthProvider(
314
- issuer_url=os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000"),
315
- external_provider="google",
316
- external_client_id=os.getenv("GOOGLE_CLIENT_ID", ""),
317
- external_client_secret=os.getenv("GOOGLE_CLIENT_SECRET", ""),
318
- external_authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
319
- external_token_url="https://oauth2.googleapis.com/token",
320
- external_userinfo_url="https://www.googleapis.com/oauth2/v1/userinfo",
321
- )