appkit-assistant 0.14.0__py3-none-any.whl → 0.15.0__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.
@@ -1,6 +1,7 @@
1
1
  """Dialog components for MCP server management."""
2
2
 
3
3
  import logging
4
+ from collections.abc import AsyncGenerator
4
5
  from typing import Any
5
6
 
6
7
  import reflex as rx
@@ -8,7 +9,8 @@ from reflex.vars import var_operation, var_operation_return
8
9
  from reflex.vars.base import RETURN, CustomVarOperationReturn
9
10
 
10
11
  import appkit_mantine as mn
11
- from appkit_assistant.backend.models import MCPServer
12
+ from appkit_assistant.backend.mcp_auth_service import MCPAuthService
13
+ from appkit_assistant.backend.models import MCPAuthType, MCPServer
12
14
  from appkit_assistant.state.mcp_server_state import MCPServerState
13
15
  from appkit_ui.components.dialogs import (
14
16
  delete_dialog,
@@ -19,6 +21,9 @@ from appkit_ui.components.form_inputs import form_field
19
21
 
20
22
  logger = logging.getLogger(__name__)
21
23
 
24
+ AUTH_TYPE_API_KEY = "api_key"
25
+ AUTH_TYPE_OAUTH = "oauth"
26
+
22
27
 
23
28
  class ValidationState(rx.State):
24
29
  url: str = ""
@@ -26,10 +31,25 @@ class ValidationState(rx.State):
26
31
  desciption: str = ""
27
32
  prompt: str = ""
28
33
 
34
+ # Authentication type selection
35
+ auth_type: str = AUTH_TYPE_API_KEY
36
+
37
+ # OAuth fields
38
+ oauth_client_id: str = ""
39
+ oauth_client_secret: str = ""
40
+
41
+ # Discovered metadata
42
+ oauth_issuer: str = ""
43
+ oauth_authorize_url: str = ""
44
+ oauth_token_url: str = ""
45
+ oauth_scopes: str = ""
46
+
29
47
  url_error: str = ""
30
48
  name_error: str = ""
31
49
  description_error: str = ""
32
50
  prompt_error: str = ""
51
+ oauth_client_id_error: str = ""
52
+ oauth_client_secret_error: str = ""
33
53
 
34
54
  @rx.event
35
55
  def initialize(self, server: MCPServer | None = None) -> None:
@@ -40,16 +60,53 @@ class ValidationState(rx.State):
40
60
  self.name = ""
41
61
  self.desciption = ""
42
62
  self.prompt = ""
63
+ self.auth_type = AUTH_TYPE_API_KEY
64
+ self.oauth_client_id = ""
65
+ self.oauth_client_secret = ""
66
+ self.oauth_issuer = ""
67
+ self.oauth_authorize_url = ""
68
+ self.oauth_token_url = ""
69
+ self.oauth_scopes = ""
43
70
  else:
44
71
  self.url = server.url
45
72
  self.name = server.name
46
73
  self.desciption = server.description
47
74
  self.prompt = server.prompt or ""
75
+ # Determine auth type from server
76
+ if server.oauth_client_id:
77
+ self.auth_type = AUTH_TYPE_OAUTH
78
+ self.oauth_client_id = server.oauth_client_id or ""
79
+ self.oauth_client_secret = server.oauth_client_secret or ""
80
+ else:
81
+ self.auth_type = AUTH_TYPE_API_KEY
82
+ self.oauth_client_id = ""
83
+ self.oauth_client_secret = ""
84
+
85
+ # Load discovered metadata
86
+ self.oauth_issuer = server.oauth_issuer or ""
87
+ self.oauth_authorize_url = server.oauth_authorize_url or ""
88
+ self.oauth_token_url = server.oauth_token_url or ""
89
+ self.oauth_scopes = server.oauth_scopes or ""
48
90
 
49
91
  self.url_error = ""
50
92
  self.name_error = ""
51
93
  self.description_error = ""
52
94
  self.prompt_error = ""
95
+ self.oauth_client_id_error = ""
96
+ self.oauth_client_secret_error = ""
97
+
98
+ @rx.event
99
+ async def set_auth_type(self, auth_type: str) -> AsyncGenerator[Any, Any]:
100
+ """Set the authentication type."""
101
+ self.auth_type = auth_type
102
+ # Clear OAuth errors when switching to API key mode
103
+ if auth_type == AUTH_TYPE_API_KEY:
104
+ self.oauth_client_id_error = ""
105
+ self.oauth_client_secret_error = ""
106
+ elif auth_type == AUTH_TYPE_OAUTH:
107
+ # Trigger discovery
108
+ async for event in self.check_discovery():
109
+ yield event
53
110
 
54
111
  @rx.event
55
112
  def validate_url(self) -> None:
@@ -91,21 +148,44 @@ class ValidationState(rx.State):
91
148
  else:
92
149
  self.prompt_error = ""
93
150
 
151
+ @rx.event
152
+ def validate_oauth_client_id(self) -> None:
153
+ """Validate the OAuth client ID field."""
154
+ # Client ID might be optional for some public clients or implicit flows
155
+ # so we don't enforce it strictly here, but warn if missing for standard flows
156
+ self.oauth_client_id_error = ""
157
+
158
+ @rx.event
159
+ def validate_oauth_client_secret(self) -> None:
160
+ """Validate the OAuth client secret field."""
161
+ # Client Secret is optional for Public Clients (PKCE)
162
+ self.oauth_client_secret_error = ""
163
+
94
164
  @rx.var
95
165
  def has_errors(self) -> bool:
96
166
  """Check if the form can be submitted."""
97
- return bool(
167
+ base_errors = bool(
98
168
  self.url_error
99
169
  or self.name_error
100
170
  or self.description_error
101
171
  or self.prompt_error
102
172
  )
173
+ if self.auth_type == AUTH_TYPE_OAUTH:
174
+ return base_errors or bool(
175
+ self.oauth_client_id_error or self.oauth_client_secret_error
176
+ )
177
+ return base_errors
103
178
 
104
179
  @rx.var
105
180
  def prompt_remaining(self) -> int:
106
181
  """Calculate remaining characters for prompt field."""
107
182
  return 2000 - len(self.prompt or "")
108
183
 
184
+ @rx.var
185
+ def is_oauth_mode(self) -> bool:
186
+ """Check if OAuth mode is selected."""
187
+ return self.auth_type == AUTH_TYPE_OAUTH
188
+
109
189
  def set_url(self, url: str) -> None:
110
190
  """Set the URL and validate it."""
111
191
  self.url = url
@@ -126,6 +206,68 @@ class ValidationState(rx.State):
126
206
  self.prompt = prompt
127
207
  self.validate_prompt()
128
208
 
209
+ def set_oauth_client_id(self, client_id: str) -> None:
210
+ """Set the OAuth client ID and validate it."""
211
+ self.oauth_client_id = client_id
212
+ self.validate_oauth_client_id()
213
+
214
+ def set_oauth_client_secret(self, client_secret: str) -> None:
215
+ """Set the OAuth client secret and validate it."""
216
+ self.oauth_client_secret = client_secret
217
+ self.validate_oauth_client_secret()
218
+
219
+ def set_oauth_issuer(self, value: str) -> None:
220
+ """Set the OAuth issuer."""
221
+ self.oauth_issuer = value
222
+
223
+ def set_oauth_authorize_url(self, value: str) -> None:
224
+ """Set the OAuth authorization URL."""
225
+ self.oauth_authorize_url = value
226
+
227
+ def set_oauth_token_url(self, value: str) -> None:
228
+ """Set the OAuth token URL."""
229
+ self.oauth_token_url = value
230
+
231
+ def set_oauth_scopes(self, value: str) -> None:
232
+ """Set the OAuth scopes."""
233
+ self.oauth_scopes = value
234
+
235
+ async def check_discovery(self) -> AsyncGenerator[Any, Any]:
236
+ """Check for OAuth configuration at the given URL."""
237
+ if not self.url or self.url_error:
238
+ return
239
+
240
+ try:
241
+ # Create a throwaway service just for discovery
242
+ service = MCPAuthService(redirect_uri="")
243
+ result = await service.discover_oauth_config(self.url)
244
+ await service.close()
245
+
246
+ if result.error:
247
+ # No OAuth or error - stick to current settings or do nothing
248
+ logger.debug("OAuth discovery failed: %s", result.error)
249
+ return
250
+
251
+ # OAuth found! Update state
252
+ self.oauth_issuer = result.issuer or ""
253
+ self.oauth_authorize_url = result.authorization_endpoint or ""
254
+ self.oauth_token_url = result.token_endpoint or ""
255
+ self.oauth_scopes = " ".join(result.scopes_supported or [])
256
+
257
+ # Switch to OAuth mode and notify user
258
+ self.auth_type = AUTH_TYPE_OAUTH
259
+ yield rx.toast.success(
260
+ f"OAuth 2.0 Konfiguration gefunden: {self.oauth_issuer}",
261
+ position="top-right",
262
+ )
263
+ # Clear OAuth errors as we just switched and fields are empty
264
+ # (user needs to fill them)
265
+ self.oauth_client_id_error = ""
266
+ self.oauth_client_secret_error = ""
267
+
268
+ except Exception as e:
269
+ logger.error("Error during OAuth discovery: %s", e)
270
+
129
271
 
130
272
  @var_operation
131
273
  def json(obj: rx.Var, indent: int = 4) -> CustomVarOperationReturn[RETURN]:
@@ -135,6 +277,179 @@ def json(obj: rx.Var, indent: int = 4) -> CustomVarOperationReturn[RETURN]:
135
277
  )
136
278
 
137
279
 
280
+ def _auth_type_selector() -> rx.Component:
281
+ """Radio for selecting authentication type."""
282
+ return rx.box(
283
+ rx.text("Authentifizierung", size="2", weight="medium", mb="2"),
284
+ rx.radio_group.root(
285
+ rx.flex(
286
+ rx.flex(
287
+ rx.radio_group.item(value=AUTH_TYPE_API_KEY),
288
+ rx.text("HTTP Headers", size="2"),
289
+ align="center",
290
+ spacing="2",
291
+ ),
292
+ rx.flex(
293
+ rx.radio_group.item(value=AUTH_TYPE_OAUTH),
294
+ rx.text("OAuth 2.0", size="2"),
295
+ align="center",
296
+ spacing="2",
297
+ ),
298
+ spacing="4",
299
+ ),
300
+ value=ValidationState.auth_type,
301
+ on_change=ValidationState.set_auth_type,
302
+ name="auth_type",
303
+ ),
304
+ width="100%",
305
+ mb="3",
306
+ )
307
+
308
+
309
+ def _api_key_auth_fields(server: MCPServer | None = None) -> rx.Component:
310
+ """Fields for API key / HTTP headers authentication."""
311
+ is_edit_mode = server is not None
312
+ return rx.cond(
313
+ ~ValidationState.is_oauth_mode,
314
+ mn.form.json(
315
+ name="headers_json",
316
+ label="HTTP Headers",
317
+ description=(
318
+ "Geben Sie die HTTP-Header im JSON-Format ein. "
319
+ 'Beispiel: {"Content-Type": "application/json", '
320
+ '"Authorization": "Bearer token"}'
321
+ ),
322
+ placeholder="{}",
323
+ validation_error="Ungültiges JSON",
324
+ default_value=json(server.headers) if is_edit_mode else "{}",
325
+ format_on_blur=True,
326
+ autosize=True,
327
+ min_rows=4,
328
+ max_rows=6,
329
+ width="100%",
330
+ ),
331
+ rx.fragment(),
332
+ )
333
+
334
+
335
+ def _oauth_auth_fields(server: MCPServer | None = None) -> rx.Component:
336
+ """Fields for OAuth 2.0 authentication."""
337
+ is_edit_mode = server is not None
338
+ return rx.cond(
339
+ ValidationState.is_oauth_mode,
340
+ rx.flex(
341
+ rx.box(
342
+ rx.callout(
343
+ "OAuth 2.0 ermöglicht eine sichere Anmeldung über den "
344
+ "Identitätsanbieter des MCP-Servers. Die OAuth-Endpunkte "
345
+ "können automatisch ermittelt oder manuell konfiguriert werden.",
346
+ icon="info",
347
+ size="1",
348
+ color="blue",
349
+ ),
350
+ width="100%",
351
+ mb="3",
352
+ ),
353
+ # Primary Fields (Client ID / Secret)
354
+ form_field(
355
+ name="oauth_client_id",
356
+ icon="key",
357
+ label="Client-ID",
358
+ hint="Die OAuth Client-ID (optional für Public Clients)",
359
+ type="text",
360
+ placeholder="client-id-xxx",
361
+ default_value=server.oauth_client_id if is_edit_mode else "",
362
+ value=ValidationState.oauth_client_id,
363
+ required=False,
364
+ on_change=ValidationState.set_oauth_client_id,
365
+ on_blur=ValidationState.validate_oauth_client_id,
366
+ validation_error=ValidationState.oauth_client_id_error,
367
+ autocomplete="off",
368
+ ),
369
+ form_field(
370
+ name="oauth_client_secret",
371
+ icon="lock",
372
+ label="Client-Secret",
373
+ hint="Das OAuth Client-Secret (optional für Public Clients)",
374
+ type="password",
375
+ placeholder="••••••••",
376
+ default_value=server.oauth_client_secret if is_edit_mode else "",
377
+ value=ValidationState.oauth_client_secret,
378
+ required=False,
379
+ on_change=ValidationState.set_oauth_client_secret,
380
+ on_blur=ValidationState.validate_oauth_client_secret,
381
+ validation_error=ValidationState.oauth_client_secret_error,
382
+ autocomplete="off",
383
+ ),
384
+ rx.text("OAuth Endpunkte & Scopes", size="2", weight="medium", mb="1"),
385
+ # Additional Discovery Fields (Editable)
386
+ form_field(
387
+ name="oauth_issuer",
388
+ icon="globe",
389
+ label="Issuer (Aussteller)",
390
+ hint="Die URL des OAuth Identity Providers",
391
+ type="text",
392
+ placeholder="https://auth.example.com",
393
+ default_value=server.oauth_issuer if is_edit_mode else "",
394
+ value=ValidationState.oauth_issuer,
395
+ required=False,
396
+ on_change=ValidationState.set_oauth_issuer,
397
+ ),
398
+ form_field(
399
+ name="oauth_authorize_url",
400
+ icon="arrow-right-left",
401
+ label="Authorization URL",
402
+ hint="Endpoint für den Login-Dialog",
403
+ type="text",
404
+ placeholder="https://auth.example.com/authorize",
405
+ default_value=server.oauth_authorize_url if is_edit_mode else "",
406
+ value=ValidationState.oauth_authorize_url,
407
+ required=False,
408
+ on_change=ValidationState.set_oauth_authorize_url,
409
+ ),
410
+ form_field(
411
+ name="oauth_token_url",
412
+ icon="key-round",
413
+ label="Token URL",
414
+ hint="Endpoint zum Tausch von Code gegen Token",
415
+ type="text",
416
+ placeholder="https://auth.example.com/token",
417
+ default_value=server.oauth_token_url if is_edit_mode else "",
418
+ value=ValidationState.oauth_token_url,
419
+ required=False,
420
+ on_change=ValidationState.set_oauth_token_url,
421
+ ),
422
+ form_field(
423
+ name="oauth_scopes",
424
+ icon="list-checks",
425
+ label="Scopes",
426
+ hint="Berechtigungen (Scopes), durch Leerzeichen getrennt",
427
+ type="text",
428
+ placeholder="openid profile email",
429
+ default_value=server.oauth_scopes if is_edit_mode else "",
430
+ value=ValidationState.oauth_scopes,
431
+ required=False,
432
+ on_change=ValidationState.set_oauth_scopes,
433
+ ),
434
+ # Hidden field to pass auth_type to form submission
435
+ rx.el.input(
436
+ type="hidden",
437
+ name="auth_type",
438
+ value=MCPAuthType.OAUTH_DISCOVERY,
439
+ ),
440
+ direction="column",
441
+ spacing="1",
442
+ width="100%",
443
+ ),
444
+ # Hidden field for non-OAuth mode
445
+ rx.el.input(
446
+ type="hidden",
447
+ name="auth_type",
448
+ value=MCPAuthType.API_KEY,
449
+ ),
450
+ )
451
+
452
+
138
453
  def mcp_server_form_fields(server: MCPServer | None = None) -> rx.Component:
139
454
  """Reusable form fields for MCP server add/update dialogs."""
140
455
  is_edit_mode = server is not None
@@ -181,7 +496,7 @@ def mcp_server_form_fields(server: MCPServer | None = None) -> rx.Component:
181
496
  default_value=server.url if is_edit_mode else "",
182
497
  required=True,
183
498
  on_change=ValidationState.set_url,
184
- on_blur=ValidationState.validate_url,
499
+ on_blur=[ValidationState.validate_url, ValidationState.check_discovery],
185
500
  validation_error=ValidationState.url_error,
186
501
  ),
187
502
  rx.flex(
@@ -225,23 +540,10 @@ def mcp_server_form_fields(server: MCPServer | None = None) -> rx.Component:
225
540
  spacing="0",
226
541
  width="100%",
227
542
  ),
228
- mn.form.json(
229
- name="headers_json",
230
- label="HTTP Headers",
231
- description=(
232
- "Geben Sie die HTTP-Header im JSON-Format ein. "
233
- 'Beispiel: {"Content-Type": "application/json", '
234
- '"Authorization": "Bearer token"}'
235
- ),
236
- placeholder="{}",
237
- validation_error="Ungültiges JSON",
238
- default_value=json(server.headers) if is_edit_mode else "{}",
239
- format_on_blur=True,
240
- autosize=True,
241
- min_rows=4,
242
- max_rows=6,
243
- width="100%",
244
- ),
543
+ # Authentication type selector and conditional fields
544
+ _auth_type_selector(),
545
+ _api_key_auth_fields(server),
546
+ _oauth_auth_fields(server),
245
547
  ]
246
548
 
247
549
  return rx.flex(
@@ -329,7 +631,11 @@ def update_mcp_server_dialog(server: MCPServer) -> rx.Component:
329
631
  ),
330
632
  rx.flex(
331
633
  rx.form.root(
332
- mcp_server_form_fields(server),
634
+ mn.scroll_area(
635
+ mcp_server_form_fields(server),
636
+ height="60vh",
637
+ width="100%",
638
+ ),
333
639
  dialog_buttons(
334
640
  "MCP Server aktualisieren",
335
641
  has_errors=ValidationState.has_errors,
@@ -23,6 +23,68 @@ message_styles = {
23
23
  }
24
24
 
25
25
 
26
+ class AuthCardComponent:
27
+ """Component for displaying MCP OAuth authentication cards."""
28
+
29
+ @staticmethod
30
+ def render() -> rx.Component:
31
+ """Render the auth required card when authentication is needed."""
32
+ return rx.cond(
33
+ ThreadState.show_auth_card,
34
+ rx.hstack(
35
+ rx.avatar(
36
+ fallback="🔐",
37
+ size="3",
38
+ variant="soft",
39
+ radius="full",
40
+ margin_top="16px",
41
+ ),
42
+ rx.card(
43
+ rx.vstack(
44
+ rx.hstack(
45
+ rx.text(
46
+ "Anmeldung erforderlich",
47
+ weight="bold",
48
+ size="3",
49
+ ),
50
+ spacing="2",
51
+ ),
52
+ rx.text(
53
+ rx.text.span(ThreadState.pending_auth_server_name),
54
+ " benötigt Ihre Anmeldung, um fortzufahren.",
55
+ size="2",
56
+ color=rx.color("gray", 11),
57
+ ),
58
+ rx.hstack(
59
+ rx.button(
60
+ "Anmelden",
61
+ on_click=ThreadState.start_mcp_oauth,
62
+ color_scheme="amber",
63
+ variant="solid",
64
+ ),
65
+ rx.button(
66
+ "Abbrechen",
67
+ on_click=ThreadState.dismiss_auth_card,
68
+ color_scheme="gray",
69
+ variant="solid",
70
+ ),
71
+ spacing="3",
72
+ margin_top="2",
73
+ ),
74
+ spacing="3",
75
+ padding="4",
76
+ ),
77
+ size="2",
78
+ variant="surface",
79
+ max_width="400px",
80
+ margin_top="16px",
81
+ ),
82
+ style=message_styles,
83
+ ),
84
+ rx.fragment(),
85
+ )
86
+
87
+
26
88
  class MessageComponent:
27
89
  @staticmethod
28
90
  def human_message(message: str) -> rx.Component:
@@ -6,7 +6,7 @@ import reflex as rx
6
6
  import appkit_mantine as mn
7
7
  from appkit_assistant.backend.models import Message, MessageType
8
8
  from appkit_assistant.components import composer
9
- from appkit_assistant.components.message import MessageComponent
9
+ from appkit_assistant.components.message import AuthCardComponent, MessageComponent
10
10
  from appkit_assistant.components.threadlist import ThreadList
11
11
  from appkit_assistant.state.thread_state import ThreadState
12
12
 
@@ -81,6 +81,8 @@ class Assistant:
81
81
  messages,
82
82
  lambda message: MessageComponent.render_message(message),
83
83
  ),
84
+ # Auth card for MCP OAuth flow
85
+ AuthCardComponent.render(),
84
86
  rx.spacer(
85
87
  id="scroll-anchor",
86
88
  display="hidden",
@@ -153,6 +155,102 @@ class Assistant:
153
155
  # ThreadState.set_suggestions(suggestions)
154
156
 
155
157
  return rx.flex(
158
+ # Hidden element with user_id for OAuth validation
159
+ rx.el.input(
160
+ id="mcp-oauth-user-id",
161
+ type="hidden",
162
+ value=ThreadState.current_user_id,
163
+ ),
164
+ # Hidden button for OAuth callback - triggered by storage event
165
+ rx.el.button(
166
+ id="mcp-oauth-success-trigger",
167
+ style={"display": "none"},
168
+ on_click=lambda: ThreadState.handle_mcp_oauth_success_from_js(),
169
+ ),
170
+ # OAuth listener for localStorage changes (cross-window)
171
+ rx.script(
172
+ """
173
+ (function() {
174
+ // Prevent double processing
175
+ if (window._mcpOAuthListenerInstalled) {
176
+ console.log('[OAuth] Listener already installed, skipping');
177
+ return;
178
+ }
179
+ window._mcpOAuthListenerInstalled = true;
180
+ var processing = false;
181
+
182
+ function getCurrentUserId() {
183
+ var el = document.getElementById('mcp-oauth-user-id');
184
+ return el ? el.value : '';
185
+ }
186
+ function processOAuthResult(data) {
187
+ if (processing) {
188
+ console.log('[OAuth] Already processing, skip');
189
+ return false;
190
+ }
191
+ processing = true;
192
+
193
+ var currentUserId = getCurrentUserId();
194
+ console.log('[OAuth] Processing, userId:', data.userId,
195
+ 'current:', currentUserId);
196
+ // Security: only process if user_id matches (or not set)
197
+ if (data.userId && currentUserId &&
198
+ String(data.userId) !== String(currentUserId)) {
199
+ console.log('[OAuth] Ignoring - user mismatch');
200
+ processing = false;
201
+ return false;
202
+ }
203
+ window._mcpOAuthData = data;
204
+ var btn = document.getElementById(
205
+ 'mcp-oauth-success-trigger'
206
+ );
207
+ if (btn) {
208
+ console.log('[OAuth] Clicking trigger button');
209
+ btn.click();
210
+ }
211
+ // Reset after short delay to allow for page navigation
212
+ setTimeout(function() { processing = false; }, 5000);
213
+ return true;
214
+ }
215
+ function checkLocalStorage() {
216
+ if (processing) return false;
217
+ var stored = localStorage.getItem('mcp-oauth-result');
218
+ if (stored) {
219
+ console.log('[OAuth] Found in localStorage');
220
+ try {
221
+ var data = JSON.parse(stored);
222
+ if (data.type === 'mcp-oauth-success') {
223
+ localStorage.removeItem('mcp-oauth-result');
224
+ return processOAuthResult(data);
225
+ }
226
+ } catch(e) { console.error('[OAuth] Parse error:', e); }
227
+ }
228
+ return false;
229
+ }
230
+ console.log('[OAuth] Installing listeners');
231
+ window.addEventListener('storage', function(event) {
232
+ if (event.key === 'mcp-oauth-result') {
233
+ checkLocalStorage();
234
+ }
235
+ });
236
+ window.addEventListener('focus', function() {
237
+ checkLocalStorage();
238
+ });
239
+ document.addEventListener('visibilitychange', function() {
240
+ if (!document.hidden) checkLocalStorage();
241
+ });
242
+ var intervalId = setInterval(function() {
243
+ if (checkLocalStorage()) clearInterval(intervalId);
244
+ }, 2000);
245
+ checkLocalStorage();
246
+ window.addEventListener('message', function(event) {
247
+ if (event.data && event.data.type === 'mcp-oauth-success') {
248
+ processOAuthResult(event.data);
249
+ }
250
+ });
251
+ })();
252
+ """
253
+ ),
156
254
  rx.cond(
157
255
  ThreadState.messages,
158
256
  Assistant.messages(