appkit-assistant 0.14.1__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.
- appkit_assistant/backend/mcp_auth_service.py +796 -0
- appkit_assistant/backend/model_manager.py +2 -1
- appkit_assistant/backend/models.py +43 -0
- appkit_assistant/backend/processors/openai_responses_processor.py +259 -25
- appkit_assistant/backend/repositories.py +1 -1
- appkit_assistant/backend/system_prompt_cache.py +5 -5
- appkit_assistant/components/mcp_server_dialogs.py +327 -21
- appkit_assistant/components/message.py +62 -0
- appkit_assistant/components/thread.py +99 -1
- appkit_assistant/state/mcp_server_state.py +42 -1
- appkit_assistant/state/system_prompt_state.py +4 -4
- appkit_assistant/state/thread_list_state.py +3 -3
- appkit_assistant/state/thread_state.py +190 -28
- {appkit_assistant-0.14.1.dist-info → appkit_assistant-0.15.0.dist-info}/METADATA +1 -1
- appkit_assistant-0.15.0.dist-info/RECORD +29 -0
- appkit_assistant-0.14.1.dist-info/RECORD +0 -28
- {appkit_assistant-0.14.1.dist-info → appkit_assistant-0.15.0.dist-info}/WHEEL +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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(
|